Add users by country to analytics dashboard.

Expose by_country breakdown on GET /api/v1/analytics/dashboard from users.country.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-24 01:47:41 -07:00
parent e957eacf80
commit 56089fa8fd
5 changed files with 65 additions and 1 deletions

View File

@ -108,6 +108,16 @@ WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.nar
GROUP BY COALESCE(NULLIF(TRIM(u.language_challange), ''), 'unknown')
ORDER BY count DESC;
-- name: AnalyticsUsersByCountry :many
SELECT
COALESCE(NULLIF(TRIM(u.country), ''), 'unknown')::text AS country,
COUNT(*)::bigint AS count
FROM users u
WHERE (sqlc.narg('range_start')::timestamptz IS NULL OR u.created_at >= sqlc.narg('range_start')::timestamptz)
AND (sqlc.narg('range_end')::timestamptz IS NULL OR u.created_at < sqlc.narg('range_end')::timestamptz)
GROUP BY COALESCE(NULLIF(TRIM(u.country), ''), 'unknown')
ORDER BY count DESC;
-- name: AnalyticsUsersByRegion :many
SELECT
COALESCE(u.region, 'unknown') AS region,

View File

@ -1080,6 +1080,47 @@ func (q *Queries) AnalyticsUsersByAgeGroup(ctx context.Context, arg AnalyticsUse
return items, nil
}
const AnalyticsUsersByCountry = `-- name: AnalyticsUsersByCountry :many
SELECT
COALESCE(NULLIF(TRIM(u.country), ''), 'unknown')::text AS country,
COUNT(*)::bigint AS count
FROM users u
WHERE ($1::timestamptz IS NULL OR u.created_at >= $1::timestamptz)
AND ($2::timestamptz IS NULL OR u.created_at < $2::timestamptz)
GROUP BY COALESCE(NULLIF(TRIM(u.country), ''), 'unknown')
ORDER BY count DESC
`
type AnalyticsUsersByCountryParams struct {
RangeStart pgtype.Timestamptz `json:"range_start"`
RangeEnd pgtype.Timestamptz `json:"range_end"`
}
type AnalyticsUsersByCountryRow struct {
Country string `json:"country"`
Count int64 `json:"count"`
}
func (q *Queries) AnalyticsUsersByCountry(ctx context.Context, arg AnalyticsUsersByCountryParams) ([]AnalyticsUsersByCountryRow, error) {
rows, err := q.db.Query(ctx, AnalyticsUsersByCountry, arg.RangeStart, arg.RangeEnd)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AnalyticsUsersByCountryRow
for rows.Next() {
var i AnalyticsUsersByCountryRow
if err := rows.Scan(&i.Country, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const AnalyticsUsersByEducationLevel = `-- name: AnalyticsUsersByEducationLevel :many
SELECT
COALESCE(NULLIF(TRIM(u.education_level), ''), 'unknown')::text AS education_level,

View File

@ -52,6 +52,7 @@ type AnalyticsUsersSection struct {
ByLearningGoal []AnalyticsLabelCount `json:"by_learning_goal"`
ByLanguageChallange []AnalyticsLabelCount `json:"by_language_challange"`
ByKnowledgeLevel []AnalyticsLabelCount `json:"by_knowledge_level"`
ByCountry []AnalyticsLabelCount `json:"by_country"`
ByRegion []AnalyticsLabelCount `json:"by_region"`
RegistrationsLast30Days []AnalyticsTimePoint `json:"registrations_last_30_days"`

View File

@ -81,6 +81,10 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by knowledge level")
}
usersByCountry, err := h.analyticsDB.AnalyticsUsersByCountry(ctx, p.UsersByCountry)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by country")
}
usersByRegion, err := h.analyticsDB.AnalyticsUsersByRegion(ctx, p.UsersByRegion)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch users by region")
@ -198,7 +202,7 @@ func (h *Handler) GetAnalyticsDashboard(c *fiber.Ctx) error {
Users: mapUsersSection(
usersSummary, usersByRole, usersByStatus, usersByAge,
usersByEducation, usersByOccupation, usersByLearningGoal, usersByLanguageChallange,
usersByKnowledge, usersByRegion, userRegs,
usersByKnowledge, usersByCountry, usersByRegion, userRegs,
),
Subscriptions: mapSubscriptionsSection(subsSummary, subsByStatus, revenueByPlan, newSubs30),
Payments: mapPaymentsSection(paymentsSummary, paymentsByStatus, paymentsByMethod, revenue30, revenueMonthlyRows, monthlyRevenueYear),
@ -250,6 +254,7 @@ func mapUsersSection(
byLearningGoal []dbgen.AnalyticsUsersByLearningGoalRow,
byLanguageChallange []dbgen.AnalyticsUsersByLanguageChallangeRow,
byKnowledge []dbgen.AnalyticsUsersByKnowledgeLevelRow,
byCountry []dbgen.AnalyticsUsersByCountryRow,
byRegion []dbgen.AnalyticsUsersByRegionRow,
regs []dbgen.AnalyticsUserRegistrationsLast30DaysRow,
) domain.AnalyticsUsersSection {
@ -285,6 +290,10 @@ func mapUsersSection(
for i, r := range byKnowledge {
knowledge[i] = domain.AnalyticsLabelCount{Label: r.KnowledgeLevel, Count: r.Count}
}
countries := make([]domain.AnalyticsLabelCount, len(byCountry))
for i, r := range byCountry {
countries[i] = domain.AnalyticsLabelCount{Label: r.Country, Count: r.Count}
}
regions := make([]domain.AnalyticsLabelCount, len(byRegion))
for i, r := range byRegion {
regions[i] = domain.AnalyticsLabelCount{Label: r.Region, Count: r.Count}
@ -306,6 +315,7 @@ func mapUsersSection(
ByLearningGoal: learningGoals,
ByLanguageChallange: languageChallanges,
ByKnowledgeLevel: knowledge,
ByCountry: countries,
ByRegion: regions,
RegistrationsLast30Days: timePoints,
}

View File

@ -18,6 +18,7 @@ type analyticsQueryParams struct {
UsersByLearningGoal dbgen.AnalyticsUsersByLearningGoalParams
UsersByLanguageChallange dbgen.AnalyticsUsersByLanguageChallangeParams
UsersByKnowledgeLevel dbgen.AnalyticsUsersByKnowledgeLevelParams
UsersByCountry dbgen.AnalyticsUsersByCountryParams
UsersByRegion dbgen.AnalyticsUsersByRegionParams
UserRegistrationsSeries dbgen.AnalyticsUserRegistrationsLast30DaysParams
@ -68,6 +69,7 @@ func newAnalyticsQueryParams(f domain.AnalyticsDateFilter) analyticsQueryParams
UsersByLearningGoal: dbgen.AnalyticsUsersByLearningGoalParams{RangeStart: rs, RangeEnd: re},
UsersByLanguageChallange: dbgen.AnalyticsUsersByLanguageChallangeParams{RangeStart: rs, RangeEnd: re},
UsersByKnowledgeLevel: dbgen.AnalyticsUsersByKnowledgeLevelParams{RangeStart: rs, RangeEnd: re},
UsersByCountry: dbgen.AnalyticsUsersByCountryParams{RangeStart: rs, RangeEnd: re},
UsersByRegion: dbgen.AnalyticsUsersByRegionParams{RangeStart: rs, RangeEnd: re},
UserRegistrationsSeries: series,