Powered by AppSignal & Oban Pro

Engagement and Retention

04-engagement-and-retention.livemd

Engagement and Retention

An active account dispatched at least one action in the period. This measures product use, not sign-in activity.

Setup

Mix.install([
  {:postgrex, "~> 0.22.0"},
  {:kino, "~> 0.19.0"},
  {:kino_vega_lite, "~> 0.1.13"}
])

Code.require_file("/opt/emisar/product_analytics.exs")
alias EmisarProductAnalytics, as: Analytics
db = Analytics.connect!()

Active accounts

activity =
  Analytics.query(db, """
  SELECT
    count(DISTINCT account_id) FILTER (WHERE inserted_at >= now() - interval '1 day') AS active_1d,
    count(DISTINCT account_id) FILTER (WHERE inserted_at >= now() - interval '7 days') AS active_7d,
    count(DISTINCT account_id) FILTER (WHERE inserted_at >= now() - interval '30 days') AS active_30d,
    count(DISTINCT account_id) FILTER (WHERE inserted_at >= now() - interval '90 days') AS active_90d
  FROM action_runs
  """)
  |> hd()

Analytics.kpis([
  {"Active accounts, 1d", activity["active_1d"]},
  {"Active accounts, 7d", activity["active_7d"]},
  {"Active accounts, 30d", activity["active_30d"]},
  {"Active accounts, 90d", activity["active_90d"]}
])

Weekly active accounts

weekly_active =
  Analytics.query(db, """
  WITH weeks AS (
    SELECT generate_series(date_trunc('week', now()) - interval '25 weeks', date_trunc('week', now()), interval '1 week') AS week
  )
  SELECT to_char(weeks.week, 'YYYY-MM-DD') AS week, count(DISTINCT ar.account_id) AS active_accounts
  FROM weeks
  LEFT JOIN action_runs ar ON ar.inserted_at >= weeks.week AND ar.inserted_at < weeks.week + interval '1 week'
  GROUP BY weeks.week
  ORDER BY weeks.week
  """)

Analytics.line(weekly_active, "week", "active_accounts", title: "Weekly active accounts", y_title: "Accounts")

Weekly cohort retention

Week zero is the signup week. Later columns count accounts that dispatched an action in that relative week.

retention =
  Analytics.query(db, """
  WITH cohorts AS (
    SELECT id, date_trunc('week', inserted_at)::date AS cohort_week
    FROM accounts
    WHERE deleted_at IS NULL AND inserted_at >= date_trunc('week', now()) - interval '12 weeks'
  ), activity AS (
    SELECT DISTINCT account_id, date_trunc('week', inserted_at)::date AS active_week
    FROM action_runs
  )
  SELECT to_char(c.cohort_week, 'YYYY-MM-DD') AS cohort_week,
    count(DISTINCT c.id) AS cohort_size,
    count(DISTINCT c.id) FILTER (WHERE a.active_week = c.cohort_week) AS week_0,
    count(DISTINCT c.id) FILTER (WHERE a.active_week = c.cohort_week + 7) AS week_1,
    count(DISTINCT c.id) FILTER (WHERE a.active_week = c.cohort_week + 14) AS week_2,
    count(DISTINCT c.id) FILTER (WHERE a.active_week = c.cohort_week + 28) AS week_4,
    count(DISTINCT c.id) FILTER (WHERE a.active_week = c.cohort_week + 56) AS week_8,
    count(DISTINCT c.id) FILTER (WHERE a.active_week = c.cohort_week + 84) AS week_12
  FROM cohorts c
  LEFT JOIN activity a ON a.account_id = c.id
  GROUP BY c.cohort_week
  ORDER BY c.cohort_week
  """)

Analytics.table(retention)

Account engagement

engagement =
  Analytics.query(db, """
  SELECT a.name AS account,
    count(ar.id) FILTER (WHERE ar.inserted_at >= now() - interval '30 days') AS runs_30d,
    count(DISTINCT ar.action_id) FILTER (WHERE ar.inserted_at >= now() - interval '30 days') AS actions_30d,
    max(ar.inserted_at) AS last_run_at
  FROM accounts a
  LEFT JOIN action_runs ar ON ar.account_id = a.id
  WHERE a.deleted_at IS NULL
  GROUP BY a.id, a.name
  ORDER BY runs_30d DESC, a.name
  LIMIT 500
  """)

Analytics.table(engagement)