Powered by AppSignal & Oban Pro

Growth and Activation

02-growth-and-activation.livemd

Growth and Activation

Activation means the account dispatched its first action. Runner registration is shown as the preceding setup milestone.

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!()

90-day activation funnel

funnel =
  Analytics.query(db, """
  WITH cohort AS (
    SELECT id, inserted_at FROM accounts
    WHERE deleted_at IS NULL AND inserted_at >= now() - interval '90 days'
  )
  SELECT stage, accounts
  FROM (
    SELECT 1 AS position, 'Created account' AS stage, count(*) AS accounts FROM cohort
    UNION ALL
    SELECT 2, 'Registered runner', count(*) FROM cohort c
      WHERE EXISTS (SELECT 1 FROM runners r WHERE r.account_id = c.id AND r.deleted_at IS NULL)
    UNION ALL
    SELECT 3, 'Dispatched action', count(*) FROM cohort c
      WHERE EXISTS (SELECT 1 FROM action_runs ar WHERE ar.account_id = c.id)
    UNION ALL
    SELECT 4, 'Dispatched MCP action', count(*) FROM cohort c
      WHERE EXISTS (SELECT 1 FROM action_runs ar WHERE ar.account_id = c.id AND ar.source = 'mcp')
  ) stages
  ORDER BY position
  """)

Analytics.bar(funnel, "stage", "accounts", title: "Account activation funnel", sort: nil, y_title: "Accounts")

Time to activation

activation =
  Analytics.query(db, """
  WITH milestones AS (
    SELECT a.id, a.name, a.inserted_at,
      (SELECT min(r.inserted_at) FROM runners r WHERE r.account_id = a.id AND r.deleted_at IS NULL) AS first_runner_at,
      (SELECT min(ar.inserted_at) FROM action_runs ar WHERE ar.account_id = a.id) AS first_run_at
    FROM accounts a
    WHERE a.deleted_at IS NULL
  )
  SELECT name,
    inserted_at AS account_created_at,
    first_runner_at,
    first_run_at,
    round(extract(epoch FROM first_runner_at - inserted_at) / 3600.0, 1) AS hours_to_runner,
    round(extract(epoch FROM first_run_at - inserted_at) / 3600.0, 1) AS hours_to_first_run
  FROM milestones
  ORDER BY inserted_at DESC
  LIMIT 200
  """)

Analytics.table(activation)

Signup cohorts

cohorts =
  Analytics.query(db, """
  SELECT to_char(date_trunc('week', a.inserted_at), 'YYYY-MM-DD') AS cohort_week,
    count(*) AS accounts,
    count(*) FILTER (WHERE EXISTS (
      SELECT 1 FROM action_runs ar
      WHERE ar.account_id = a.id AND ar.inserted_at <= a.inserted_at + interval '7 days'
    )) AS activated_7d,
    round(100.0 * count(*) FILTER (WHERE EXISTS (
      SELECT 1 FROM action_runs ar
      WHERE ar.account_id = a.id AND ar.inserted_at <= a.inserted_at + interval '7 days'
    )) / NULLIF(count(*), 0), 1)::float8 AS activation_rate_7d
  FROM accounts a
  WHERE a.deleted_at IS NULL AND a.inserted_at >= now() - interval '26 weeks'
  GROUP BY date_trunc('week', a.inserted_at)
  ORDER BY date_trunc('week', a.inserted_at)
  """)

Analytics.table(cohorts)