Retention Grid
Query
-- ── Subscription type classification ────────────────────────
WITH subs AS (
SELECT
s.ID,
CONVERT_TIMEZONE('UTC','America/Los_Angeles', TO_TIMESTAMP(s.CREATED))::DATE AS created_date,
CASE WHEN s.ENDED_AT IS NOT NULL
THEN CONVERT_TIMEZONE('UTC','America/Los_Angeles', TO_TIMESTAMP(s.ENDED_AT::NUMBER))::DATE
END AS ended_date,
CASE
WHEN s.PLAN:amount::NUMBER = 0 THEN 'Free'
WHEN s.PLAN:amount::NUMBER > 0 AND s.PLAN:interval::VARCHAR = 'month' THEN 'Paid Monthly'
WHEN s.PLAN:amount::NUMBER > 0 AND s.PLAN:interval::VARCHAR = 'year' THEN 'Paid Annual'
END AS sub_type
FROM RAW_DB.STRIPE.SUBSCRIPTIONS s
WHERE s.LIVEMODE = true
AND s.STATUS != 'incomplete_expired'
AND CASE
WHEN s.PLAN:amount::NUMBER = 0 THEN 'Free'
WHEN s.PLAN:amount::NUMBER > 0 AND s.PLAN:interval::VARCHAR = 'month' THEN 'Paid Monthly'
WHEN s.PLAN:amount::NUMBER > 0 AND s.PLAN:interval::VARCHAR = 'year' THEN 'Paid Annual'
END IS NOT NULL
),
-- ── Eligible devices (filtered by sub type) ─────────────────
eligible_devices AS (
SELECT DISTINCT
d.DEVICE_ID,
d.STRIPE_SUBSCRIPTION_ID,
CONVERT_TIMEZONE('UTC','America/Los_Angeles', d.CREATED_AT)::DATE AS device_created_date,
s.sub_type
FROM RAW_DB.TINCAN.LEGACY_DEVICES d
JOIN subs s ON d.STRIPE_SUBSCRIPTION_ID = s.ID
WHERE d.STRIPE_SUBSCRIPTION_ID IS NOT NULL
{% if sub_type_filter is not none and sub_type_filter | length > 0 %}
AND s.sub_type IN ({{ sub_type_filter | array }})
{% endif %}
),
-- ── Usage-based retention CTEs ──────────────────────────────
base_activity AS (
SELECT
a.THIS_PARTICIPANT_DEVICE_ID,
a.CALL_DAY,
CASE
WHEN {{ time_grain }} = 'Week' THEN DATE_TRUNC('week', a.CALL_DAY)
WHEN {{ time_grain }} = 'Month' THEN DATE_TRUNC('month', a.CALL_DAY)
WHEN {{ time_grain }} = 'Quarter' THEN DATE_TRUNC('quarter', a.CALL_DAY)
END AS activity_period,
a.SUCCESSFUL_CALLS_PARTICIPATED_IN,
a.talk_time_seconds
FROM ANALYTICS_DB.ANALYTICS.PARTICIPANT_DAILY_ACTIVITY_FULL_HISTORY a
INNER JOIN eligible_devices ed
ON a.THIS_PARTICIPANT_DEVICE_ID = ed.DEVICE_ID
WHERE a.THIS_PARTICIPANT_DEVICE_ID != 3635
),
current_period AS (
SELECT
CASE
WHEN {{ time_grain }} = 'Week' THEN DATE_TRUNC('week', CURRENT_DATE)
WHEN {{ time_grain }} = 'Month' THEN DATE_TRUNC('month', CURRENT_DATE)
WHEN {{ time_grain }} = 'Quarter' THEN DATE_TRUNC('quarter', CURRENT_DATE)
END AS current_period
),
device_periodly AS (
SELECT
THIS_PARTICIPANT_DEVICE_ID,
activity_period,
SUM(SUCCESSFUL_CALLS_PARTICIPATED_IN) AS calls,
SUM(talk_time_seconds) AS talk_time_seconds
FROM base_activity
GROUP BY 1, 2
),
device_cohort AS (
SELECT
THIS_PARTICIPANT_DEVICE_ID,
MIN(activity_period) AS cohort_period
FROM device_periodly
WHERE calls >= 1
GROUP BY 1
),
cohort_sizes AS (
SELECT
cohort_period,
COUNT(DISTINCT THIS_PARTICIPANT_DEVICE_ID) AS cohort_size
FROM device_cohort
GROUP BY 1
),
period_spine AS (
SELECT DISTINCT
ba.activity_period
FROM base_activity AS ba
CROSS JOIN current_period AS cp
WHERE ba.activity_period < cp.current_period
),
cohort_period_cross AS (
SELECT
dc.THIS_PARTICIPANT_DEVICE_ID,
dc.cohort_period,
ps.activity_period
FROM device_cohort AS dc
CROSS JOIN period_spine AS ps
WHERE ps.activity_period >= dc.cohort_period
),
full_grid AS (
SELECT
cpc.THIS_PARTICIPANT_DEVICE_ID,
cpc.cohort_period,
cpc.activity_period,
COALESCE(dp.calls, 0) AS calls,
COALESCE(dp.talk_time_seconds, 0) AS talk_time_seconds
FROM cohort_period_cross AS cpc
LEFT JOIN device_periodly AS dp
ON cpc.THIS_PARTICIPANT_DEVICE_ID = dp.THIS_PARTICIPANT_DEVICE_ID
AND cpc.activity_period = dp.activity_period
),
full_grid_enriched AS (
SELECT
fg.*,
CASE
WHEN {{ time_grain }} = 'Week' THEN DATEDIFF('week', fg.cohort_period, fg.activity_period)
WHEN {{ time_grain }} = 'Month' THEN DATEDIFF('month', fg.cohort_period, fg.activity_period)
WHEN {{ time_grain }} = 'Quarter' THEN DATEDIFF('quarter', fg.cohort_period, fg.activity_period)
END AS periods_since_activation,
CASE
WHEN {{ time_grain }} = 'Week' THEN TO_CHAR(fg.cohort_period, 'YYYY-MM-DD')
WHEN {{ time_grain }} = 'Month' THEN TO_CHAR(fg.cohort_period, 'YYYY-MM')
WHEN {{ time_grain }} = 'Quarter' THEN CAST(EXTRACT(YEAR FROM fg.cohort_period) AS VARCHAR) || '-Q' || CAST(EXTRACT(QUARTER FROM fg.cohort_period) AS VARCHAR)
END AS cohort_label
FROM full_grid AS fg
),
chart_window AS (
SELECT
CASE
WHEN {{ custom_date_start }} IS NOT NULL
THEN CAST({{ custom_date_start }} AS DATE)
ELSE '7/1/2025'::DATE
END AS start_date,
CASE
WHEN {{ custom_date_end }} IS NOT NULL
THEN CAST({{ custom_date_end }} AS DATE)
ELSE CURRENT_DATE
END AS end_date
),
-- ── Network retention CTEs ──────────────────────────────────
network_device_subs AS (
SELECT
ed.STRIPE_SUBSCRIPTION_ID,
ed.device_created_date,
s.created_date AS sub_created_date,
s.ended_date AS sub_ended_date,
CASE
WHEN {{ time_grain }} = 'Week' THEN DATE_TRUNC('week', s.created_date)
WHEN {{ time_grain }} = 'Month' THEN DATE_TRUNC('month', s.created_date)
WHEN {{ time_grain }} = 'Quarter' THEN DATE_TRUNC('quarter', s.created_date)
END AS network_cohort_period
FROM eligible_devices ed
JOIN subs s ON ed.STRIPE_SUBSCRIPTION_ID = s.ID
),
network_cohort_sizes AS (
SELECT network_cohort_period, COUNT(*) AS network_cohort_size
FROM network_device_subs
GROUP BY 1
),
network_retention AS (
SELECT
nds.network_cohort_period,
ps.activity_period,
COUNT(*) AS network_retained_devices
FROM network_device_subs nds
JOIN period_spine ps
ON nds.sub_created_date < CASE
WHEN {{ time_grain }} = 'Week' THEN DATEADD('week', 1, ps.activity_period)
WHEN {{ time_grain }} = 'Month' THEN DATEADD('month', 1, ps.activity_period)
WHEN {{ time_grain }} = 'Quarter' THEN DATEADD('quarter', 1, ps.activity_period)
END
AND (nds.sub_ended_date IS NULL OR nds.sub_ended_date > ps.activity_period)
WHERE ps.activity_period >= nds.network_cohort_period
GROUP BY 1, 2
)
-- ── Final SELECT ────────────────────────────────────────────
SELECT
cs.cohort_size,
fge.cohort_period,
fge.cohort_label || ' (n=' || TO_VARCHAR(cs.cohort_size, '999,999,999') || ')' AS cohort,
fge.cohort_label || ' (n=' || TO_VARCHAR(ncs.network_cohort_size, '999,999,999') || ')' AS network_cohort,
fge.periods_since_activation,
fge.activity_period,
--Call volume & time stats for All users (default measure, includes zeros)
MEDIAN(fge.calls) AS median_calls_all,
AVG(fge.calls) AS avg_calls_all,
MEDIAN(fge.talk_time_seconds) / 60.0 AS median_talk_minutes_all,
AVG(fge.talk_time_seconds) / 60.0 AS avg_talk_minutes_all,
--Call volume & time stats for RETAINED users only
MEDIAN(CASE
WHEN fge.calls >= {{ retention_call_threshold }}
AND (fge.talk_time_seconds / 60.0) >= {{ retention_talk_minutes_threshold }}
THEN fge.calls END) AS median_calls_retained,
AVG(CASE
WHEN fge.calls >= {{ retention_call_threshold }}
AND (fge.talk_time_seconds / 60.0) >= {{ retention_talk_minutes_threshold }}
THEN fge.calls END) AS avg_calls_retained,
MEDIAN(CASE
WHEN fge.calls >= {{ retention_call_threshold }}
AND (fge.talk_time_seconds / 60.0) >= {{ retention_talk_minutes_threshold }}
THEN fge.talk_time_seconds / 60.0 END) AS median_talk_minutes_retained,
AVG(CASE
WHEN fge.calls >= {{ retention_call_threshold }}
AND (fge.talk_time_seconds / 60.0) >= {{ retention_talk_minutes_threshold }}
THEN fge.talk_time_seconds / 60.0 END) AS avg_talk_minutes_retained,
AVG(
CASE
WHEN fge.calls >= {{ retention_call_threshold }}
AND (fge.talk_time_seconds / 60.0) >= {{ retention_talk_minutes_threshold }}
THEN 1.0 ELSE 0.0
END
) AS pct_retained,
-- Network retention columns
ncs.network_cohort_size,
nr.network_retained_devices
FROM full_grid_enriched AS fge
INNER JOIN cohort_sizes AS cs
ON fge.cohort_period = cs.cohort_period
CROSS JOIN chart_window cw
LEFT JOIN network_retention nr
ON fge.cohort_period = nr.network_cohort_period
AND fge.activity_period = nr.activity_period
LEFT JOIN network_cohort_sizes ncs
ON fge.cohort_period = ncs.network_cohort_period
WHERE
fge.cohort_period >= cw.start_date
AND fge.activity_period <= cw.end_date
GROUP BY ALL
ORDER BY
fge.cohort_period,
fge.periods_since_activation
Overview
The primary query for the user retention report. Produces a cohort × period grid showing how engagement and retention evolve over time for devices on the active network. Each row represents a cohort-period combination and includes retention rates, call volume statistics, and parallel network (subscription) retention metrics.
Two distinct retention concepts are tracked in parallel:
- Usage retention: Whether a device, which has made at least one call in the past, met the configured call and talk time thresholds in a given period.
- Network retention: Whether the underlying subscription was still active in a given period, regardless of call activity.
Depends on: ANALYTICS_DB.ANALYTICS.PARTICIPANT_DAILY_ACTIVITY_FULL_HISTORY, RAW_DB.STRIPE.SUBSCRIPTIONS, RAW_DB.TINCAN.LEGACY_DEVICES
Parameters
| Parameter | Type | Description |
|---|---|---|
{{ time_grain }} |
'Week', 'Month', 'Quarter' |
Aggregation period for cohort and activity bucketing |
{{ sub_type_filter }} |
Array or none |
Optional filter to restrict analysis to specific subscription types ('Free', 'Paid Monthly', 'Paid Annual'). If empty or none, all types are included |
{{ retention_call_threshold }} |
Integer | Minimum number of calls a device must have in a period to be counted as a retained USER |
{{ retention_talk_minutes_threshold }} |
Number | Minimum total talk minutes a device must have in a period to be counted as a retained USER |
{{ custom_date_start }} |
Date or NULL |
Start of the cohort window. Defaults to 2025-07-01 if not set |
{{ custom_date_end }} |
Date or NULL |
End of the activity window. Defaults to CURRENT_DATE if not set |
Output Columns
| Column | Description |
|---|---|
cohort_size |
Number of devices in the usage cohort (users whose first call was in the cohort period) |
cohort_period |
Start date of the cohort period |
cohort |
Display label for the cohort, including cohort size (e.g. "2025-01 (n=42)") |
network_cohort |
Display label for the network cohort, including network cohort size |
periods_since_activation |
Number of periods elapsed since the device's first active period |
activity_period |
The period being measured |
median_calls_all |
Median calls per device across all devices in the cohort (including zeros) |
avg_calls_all |
Average calls per device across all devices (including zeros) |
median_talk_minutes_all |
Median talk minutes per device across all devices (including zeros) |
avg_talk_minutes_all |
Average talk minutes per device across all devices (including zeros) |
median_calls_retained |
Median calls among retained devices only |
avg_calls_retained |
Average calls among retained devices only |
median_talk_minutes_retained |
Median talk minutes among retained devices only |
avg_talk_minutes_retained |
Average talk minutes among retained devices only |
pct_retained |
Fraction of cohort devices meeting both the call and talk minute thresholds in this period |
network_cohort_size |
Number of subscriptions in the network cohort for this period |
network_retained_devices |
Number of subscriptions still active in the given activity period |
CTEs
subs
Source: RAW_DB.STRIPE.SUBSCRIPTIONS
Filters to live-mode, non-expired subscriptions and classifies each into 'Free', 'Paid Monthly', or 'Paid Annual' based on PLAN:amount and PLAN:interval. Subscriptions that don't match any classification are excluded.
eligible_devices
Source: RAW_DB.TINCAN.LEGACY_DEVICES
Joins devices to their subscription and applies the optional {{ sub_type_filter }} to restrict the analysis population. Uses Jinja conditional syntax ({% if ... %}), so the filter is only applied when values are provided.
base_activity
Source: ANALYTICS_DB.ANALYTICS.PARTICIPANT_DAILY_ACTIVITY_FULL_HISTORY
Pulls daily call activity for eligible devices and assigns each day to the appropriate period based on {{ time_grain }}. Device 3635 is explicitly excluded (test/internal device).
current_period
Computes the start of the current in-progress period. Used to exclude the current period from the retention grid, since it is incomplete.
device_periodly
Aggregates base_activity to the device × period grain, summing calls and talk time.
device_cohort
Assigns each device to its cohort period — the first period in which it had at least one call.
cohort_sizes
Counts the number of devices in each cohort period.
period_spine
Produces the distinct set of completed activity periods from base_activity, excluding the current in-progress period.
cohort_period_cross
Produces all valid cohort × activity period combinations — every cohort crossed with every period at or after its cohort start. This is the scaffold for the retention grid.
full_grid
Left-joins actual device activity onto cohort_period_cross. Devices with no activity in a given period receive 0 for calls and talk time.
full_grid_enriched
Adds periods_since_activation (periods elapsed since cohort start) and cohort_label (human-readable cohort identifier) to each row.
chart_window
Defines the cohort and activity date range for the final filter. Defaults to 2025-07-01 as the start date if {{ custom_date_start }} is not provided.
network_device_subs
Joins eligible_devices back to subs to capture subscription start and end dates alongside each device's cohort period for network retention analysis.
network_cohort_sizes
Counts subscriptions per network cohort period.
network_retention
For each network cohort × activity period combination, counts subscriptions that were still active during the activity period (i.e. created before the period ended and not yet ended at the start of the period).
Notes
- Two retention definitions run in parallel. Usage retention (
pct_retained) requires a device to meet both{{ retention_call_threshold }}and{{ retention_talk_minutes_threshold }}in a period. Network retention (network_retained_devices / network_cohort_size) only requires the subscription to still be active — no call activity needed. - "All" vs. "retained" stats. The
_allaggregate columns include every device in the cohort for that period, including those with zero activity. The_retainedcolumns include only devices that met both thresholds. This means_allmedians and averages will be pulled down by inactive devices. - Default chart start date is
2025-07-01, not2025-01-01like other reports. This likely reflects when reliable retention data begins. - Device
3635is excluded frombase_activityas a known test or internal device. sub_type_filteruses Jinja array syntax ({{ sub_type_filter | array }}), which is distinct from the standard{{ parameter }}Hex syntax used elsewhere. This enables multi-select filtering.