mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-20 00:25:21 +00:00
210 lines
6 KiB
Ruby
210 lines
6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class AnalyticsService
|
|
SIGNUP_FUNNEL_EVENTS = %w[
|
|
signup.started
|
|
signup.completed
|
|
].freeze
|
|
|
|
SIGNUP_REJECTION_EVENTS = %w[
|
|
signup.age_rejected
|
|
signup.validation_failed
|
|
signup.existing_account
|
|
].freeze
|
|
|
|
LOGIN_FUNNEL_EVENTS = %w[
|
|
login.code_sent
|
|
login.code_verified
|
|
login.completed
|
|
].freeze
|
|
|
|
# Complete user journey for program operators
|
|
PROGRAM_FUNNEL_EVENTS = %w[
|
|
signup.started
|
|
login.code_sent
|
|
signup.completed
|
|
login.completed
|
|
oauth.authorized
|
|
].freeze
|
|
|
|
def initialize(start_date:, end_date:, scenario: nil)
|
|
@start_date = start_date
|
|
@end_date = end_date
|
|
@scenario = scenario
|
|
end
|
|
|
|
# Signup funnel: started -> completed
|
|
def signup_funnel
|
|
base_scope = events_in_range.signup
|
|
|
|
SIGNUP_FUNNEL_EVENTS.each_with_object({}) do |event, result|
|
|
scope = base_scope.by_name(event)
|
|
scope = scope.by_scenario(@scenario) if @scenario
|
|
result[event] = scope.count
|
|
end
|
|
end
|
|
|
|
# Signup conversion rate
|
|
def signup_conversion_rate
|
|
funnel = signup_funnel
|
|
started = funnel["signup.started"].to_f
|
|
completed = funnel["signup.completed"].to_f
|
|
|
|
return 0 if started.zero?
|
|
((completed / started) * 100).round(2)
|
|
end
|
|
|
|
# Rejection breakdown
|
|
def rejection_breakdown
|
|
result = {}
|
|
|
|
# Age rejections by type
|
|
scope = events_in_range.by_name("signup.age_rejected")
|
|
scope = scope.by_scenario(@scenario) if @scenario
|
|
age_rejections = scope.group("properties->>'rejection_type'").count
|
|
|
|
result[:age_rejections] = {
|
|
too_old: age_rejections["too_old"] || 0,
|
|
under_13: age_rejections["under_13"] || 0
|
|
}
|
|
|
|
# Validation errors
|
|
validation_scope = events_in_range.by_name("signup.validation_failed")
|
|
validation_scope = validation_scope.by_scenario(@scenario) if @scenario
|
|
result[:validation_errors] = validation_scope.count
|
|
|
|
# Existing accounts
|
|
existing_scope = events_in_range.by_name("signup.existing_account")
|
|
existing_scope = existing_scope.by_scenario(@scenario) if @scenario
|
|
result[:existing_accounts] = existing_scope.count
|
|
|
|
result
|
|
end
|
|
|
|
# Login funnel: code_sent -> completed
|
|
def login_funnel
|
|
LOGIN_FUNNEL_EVENTS.each_with_object({}) do |event, result|
|
|
scope = events_in_range.by_name(event)
|
|
scope = scope.by_scenario(@scenario) if @scenario
|
|
result[event] = scope.count
|
|
end
|
|
end
|
|
|
|
# Dialogue funnel
|
|
def dialogue_funnel
|
|
first_scope = events_in_range.by_name("dialogue.first_interaction")
|
|
first_scope = first_scope.by_scenario(@scenario) if @scenario
|
|
|
|
promoted_scope = events_in_range.by_name("dialogue.promoted")
|
|
promoted_scope = promoted_scope.by_scenario(@scenario) if @scenario
|
|
|
|
{
|
|
first_interaction: first_scope.count,
|
|
promoted: promoted_scope.count
|
|
}
|
|
end
|
|
|
|
# OAuth funnel
|
|
def oauth_funnel
|
|
{
|
|
authorized: scoped_count("oauth.authorized"),
|
|
denied: scoped_count("oauth.denied"),
|
|
revoked: scoped_count("oauth.revoked")
|
|
}
|
|
end
|
|
|
|
# Scenario comparison
|
|
def scenario_comparison
|
|
events_in_range
|
|
.by_name("signup.completed")
|
|
.group("properties->>'scenario'")
|
|
.count
|
|
.sort_by { |_, v| -v }
|
|
.to_h
|
|
end
|
|
|
|
# Promotion breakdown by onboarding flow (queries Identity directly)
|
|
def promotion_breakdown
|
|
scope = Identity.where(created_at: @start_date..@end_date)
|
|
scope = scope.where(onboarding_scenario: @scenario) if @scenario
|
|
|
|
totals = scope.group(:onboarding_scenario).count
|
|
promoted = scope.where("promote_click_count > 0").group(:onboarding_scenario).count
|
|
|
|
totals.map do |scenario, total|
|
|
prom = promoted[scenario] || 0
|
|
rate = total > 0 ? ((prom.to_f / total) * 100).round(1) : 0
|
|
[ scenario || "default", { total: total, promoted: prom, rate: rate } ]
|
|
end.sort_by { |_, v| -v[:total] }.to_h
|
|
end
|
|
|
|
# Country breakdown
|
|
def country_breakdown
|
|
scope = events_in_range.by_name("signup.started")
|
|
scope = scope.by_scenario(@scenario) if @scenario
|
|
scope
|
|
.group("properties->>'country'")
|
|
.count
|
|
.sort_by { |_, v| -v }
|
|
.to_h
|
|
end
|
|
|
|
# Daily trends for charting
|
|
def daily_trends(event_name)
|
|
scope = events_in_range.by_name(event_name)
|
|
scope = scope.by_scenario(@scenario) if @scenario
|
|
scope.group_by_day(:time).count
|
|
end
|
|
|
|
# Overview stats - when filtering by scenario, show program-centric metrics
|
|
def overview
|
|
if @scenario
|
|
# Program-centric view: what matters is users delivered to the program
|
|
started = scoped_count("signup.started") + scoped_count("login.code_sent")
|
|
authorized = scoped_count("oauth.authorized")
|
|
conversion = started > 0 ? [ (authorized.to_f / started) * 100, 100 ].min.round(2) : 0
|
|
|
|
{
|
|
visitors: started,
|
|
new_signups: scoped_count("signup.completed"),
|
|
returning_logins: scoped_count("login.completed"),
|
|
authorized: authorized,
|
|
denied: scoped_count("oauth.denied"),
|
|
conversion_rate: conversion
|
|
}
|
|
else
|
|
# Global view: overall platform health
|
|
{
|
|
total_signups_started: scoped_count("signup.started"),
|
|
total_signups_completed: scoped_count("signup.completed"),
|
|
total_logins_completed: scoped_count("login.completed"),
|
|
total_oauth_authorized: scoped_count("oauth.authorized"),
|
|
total_verifications: scoped_count("verification.submitted"),
|
|
slack_provisioned: scoped_count("slack.provisioned"),
|
|
conversion_rate: signup_conversion_rate
|
|
}
|
|
end
|
|
end
|
|
|
|
# Program funnel: full journey from arrival to authorization
|
|
def program_funnel
|
|
{
|
|
started: scoped_count("signup.started") + scoped_count("login.code_sent"),
|
|
authenticated: scoped_count("signup.completed") + scoped_count("login.completed"),
|
|
authorized: scoped_count("oauth.authorized"),
|
|
denied: scoped_count("oauth.denied")
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def events_in_range
|
|
Ahoy::Event.in_range(@start_date, @end_date)
|
|
end
|
|
|
|
def scoped_count(event_name)
|
|
scope = events_in_range.by_name(event_name)
|
|
scope = scope.by_scenario(@scenario) if @scenario
|
|
scope.count
|
|
end
|
|
end
|