gosora/routes/panel/analytics.go

712 lines
23 KiB
Go

package panel
import (
"database/sql"
"errors"
"log"
"net/http"
"strconv"
"time"
"github.com/Azareal/Gosora/common"
"github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/Gosora/query_gen"
)
// TODO: Move this to another file, probably common/pages.go
type AnalyticsTimeRange struct {
Quantity int
Unit string
Slices int
SliceWidth int
Range string
}
func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err error) {
timeRange.Quantity = 6
timeRange.Unit = "hour"
timeRange.Slices = 12
timeRange.SliceWidth = 60 * 30
timeRange.Range = "six-hours"
switch rawTimeRange {
case "one-month":
timeRange.Quantity = 30
timeRange.Unit = "day"
timeRange.Slices = 30
timeRange.SliceWidth = 60 * 60 * 24
timeRange.Range = "one-month"
case "one-week":
timeRange.Quantity = 7
timeRange.Unit = "day"
timeRange.Slices = 14
timeRange.SliceWidth = 60 * 60 * 12
timeRange.Range = "one-week"
case "two-days": // Two days is experimental
timeRange.Quantity = 2
timeRange.Unit = "day"
timeRange.Slices = 24
timeRange.SliceWidth = 60 * 60 * 2
timeRange.Range = "two-days"
case "one-day":
timeRange.Quantity = 1
timeRange.Unit = "day"
timeRange.Slices = 24
timeRange.SliceWidth = 60 * 60
timeRange.Range = "one-day"
case "twelve-hours":
timeRange.Quantity = 12
timeRange.Slices = 24
timeRange.Range = "twelve-hours"
case "six-hours", "":
timeRange.Range = "six-hours"
default:
return timeRange, errors.New("Unknown time range")
}
return timeRange, nil
}
func analyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
viewMap[label] = 0
}
for _, value := range revLabelList {
labelList = append(labelList, value)
}
return revLabelList, labelList, viewMap
}
func analyticsRowsToViewMap(rows *sql.Rows, labelList []int64, viewMap map[int64]int64) (map[int64]int64, error) {
defer rows.Close()
for rows.Next() {
var count int64
var createdAt time.Time
err := rows.Scan(&count, &createdAt)
if err != nil {
return viewMap, err
}
var unixCreatedAt = createdAt.Unix()
// TODO: Bulk log this
if common.Dev.SuperDebug {
log.Print("count: ", count)
log.Print("createdAt: ", createdAt)
log.Print("unixCreatedAt: ", unixCreatedAt)
}
for _, value := range labelList {
if unixCreatedAt > value {
viewMap[value] += count
break
}
}
}
return viewMap, rows.Err()
}
func PreAnalyticsDetail(w http.ResponseWriter, r *http.Request, user *common.User) (*common.BasePanelPage, common.RouteError) {
basePage, ferr := buildBasePage(w, r, user, "analytics", "analytics")
if ferr != nil {
return nil, ferr
}
basePage.AddSheet("chartist/chartist.min.css")
basePage.AddScript("chartist/chartist.min.js")
basePage.AddScript("analytics.js")
return basePage, nil
}
func AnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
common.DebugLog("in panel.AnalyticsViews")
// TODO: Add some sort of analytics store / iterator?
rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
var viewItems []common.PanelAnalyticsItem
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]})
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range}
return renderTemplate("panel_analytics_views", w, r, user, &pi)
}
func AnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user common.User, route string) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
common.DebugLog("in panel.AnalyticsRouteViews")
// TODO: Validate the route is valid
rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, createdAt").Where("route = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(route)
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
var viewItems []common.PanelAnalyticsItem
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]})
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
pi := common.PanelAnalyticsRoutePage{basePage, common.SanitiseSingleLine(route), graph, viewItems, timeRange.Range}
return renderTemplate("panel_analytics_route_views", w, r, user, &pi)
}
func AnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user common.User, agent string) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
// ? Only allow valid agents? The problem with this is that agents wind up getting renamed and it would take a migration to get them all up to snuff
agent = common.SanitiseSingleLine(agent)
common.DebugLog("in panel.AnalyticsAgentViews")
// TODO: Verify the agent is valid
rows, err := qgen.NewAcc().Select("viewchunks_agents").Columns("count, createdAt").Where("browser = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(agent)
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
friendlyAgent, ok := phrases.GetUserAgentPhrase(agent)
if !ok {
friendlyAgent = agent
}
pi := common.PanelAnalyticsAgentPage{basePage, agent, friendlyAgent, graph, timeRange.Range}
return renderTemplate("panel_analytics_agent_views", w, r, user, &pi)
}
func AnalyticsForumViews(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
fid, err := strconv.Atoi(sfid)
if err != nil {
return common.LocalError("Invalid integer", w, r, user)
}
common.DebugLog("in panel.AnalyticsForumViews")
// TODO: Verify the agent is valid
rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, createdAt").Where("forum = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(fid)
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
forum, err := common.Forums.Get(fid)
if err != nil {
return common.InternalError(err, w, r)
}
pi := common.PanelAnalyticsAgentPage{basePage, sfid, forum.Name, graph, timeRange.Range}
return renderTemplate("panel_analytics_forum_views", w, r, user, &pi)
}
func AnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user common.User, system string) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
system = common.SanitiseSingleLine(system)
common.DebugLog("in panel.AnalyticsSystemViews")
// TODO: Verify the OS name is valid
rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, createdAt").Where("system = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(system)
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
friendlySystem, ok := phrases.GetOSPhrase(system)
if !ok {
friendlySystem = system
}
pi := common.PanelAnalyticsAgentPage{basePage, system, friendlySystem, graph, timeRange.Range}
return renderTemplate("panel_analytics_system_views", w, r, user, &pi)
}
func AnalyticsLanguageViews(w http.ResponseWriter, r *http.Request, user common.User, lang string) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
lang = common.SanitiseSingleLine(lang)
common.DebugLog("in panel.AnalyticsLanguageViews")
// TODO: Verify the language code is valid
rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, createdAt").Where("lang = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(lang)
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
friendlyLang, ok := phrases.GetHumanLangPhrase(lang)
if !ok {
friendlyLang = lang
}
pi := common.PanelAnalyticsAgentPage{basePage, lang, friendlyLang, graph, timeRange.Range}
return renderTemplate("panel_analytics_lang_views", w, r, user, &pi)
}
func AnalyticsReferrerViews(w http.ResponseWriter, r *http.Request, user common.User, domain string) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
common.DebugLog("in panel.AnalyticsReferrerViews")
// TODO: Verify the agent is valid
rows, err := qgen.NewAcc().Select("viewchunks_referrers").Columns("count, createdAt").Where("domain = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(domain)
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
pi := common.PanelAnalyticsAgentPage{basePage, common.SanitiseSingleLine(domain), "", graph, timeRange.Range}
return renderTemplate("panel_analytics_referrer_views", w, r, user, &pi)
}
func AnalyticsTopics(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
common.DebugLog("in panel.AnalyticsTopics")
rows, err := qgen.NewAcc().Select("topicchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
var viewItems []common.PanelAnalyticsItem
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]})
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range}
return renderTemplate("panel_analytics_topics", w, r, user, &pi)
}
func AnalyticsPosts(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user)
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange)
common.DebugLog("in panel.AnalyticsPosts")
rows, err := qgen.NewAcc().Select("postchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap)
if err != nil {
return common.InternalError(err, w, r)
}
var viewList []int64
var viewItems []common.PanelAnalyticsItem
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]})
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph)
pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range}
return renderTemplate("panel_analytics_posts", w, r, user, &pi)
}
func analyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) {
nameMap := make(map[string]int)
defer rows.Close()
for rows.Next() {
var count int
var name string
err := rows.Scan(&count, &name)
if err != nil {
return nameMap, err
}
// TODO: Bulk log this
if common.Dev.SuperDebug {
log.Print("count: ", count)
log.Print("name: ", name)
}
nameMap[name] += count
}
return nameMap, rows.Err()
}
func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
forumMap, err := analyticsRowsToNameMap(rows)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Sort this slice
var forumItems []common.PanelAnalyticsAgentsItem
for sfid, count := range forumMap {
fid, err := strconv.Atoi(sfid)
if err != nil {
return common.InternalError(err, w, r)
}
forum, err := common.Forums.Get(fid)
if err != nil {
return common.InternalError(err, w, r)
}
forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{
Agent: sfid,
FriendlyAgent: forum.Name,
Count: count,
})
}
pi := common.PanelAnalyticsAgentsPage{basePage, forumItems, timeRange.Range}
return renderTemplate("panel_analytics_forums", w, r, user, &pi)
}
func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, route").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
routeMap, err := analyticsRowsToNameMap(rows)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Sort this slice
var routeItems []common.PanelAnalyticsRoutesItem
for route, count := range routeMap {
routeItems = append(routeItems, common.PanelAnalyticsRoutesItem{
Route: route,
Count: count,
})
}
pi := common.PanelAnalyticsRoutesPage{basePage, routeItems, timeRange.Range}
return renderTemplate("panel_analytics_routes", w, r, user, &pi)
}
func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
rows, err := qgen.NewAcc().Select("viewchunks_agents").Columns("count, browser").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
agentMap, err := analyticsRowsToNameMap(rows)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Sort this slice
var agentItems []common.PanelAnalyticsAgentsItem
for agent, count := range agentMap {
aAgent, ok := phrases.GetUserAgentPhrase(agent)
if !ok {
aAgent = agent
}
agentItems = append(agentItems, common.PanelAnalyticsAgentsItem{
Agent: agent,
FriendlyAgent: aAgent,
Count: count,
})
}
pi := common.PanelAnalyticsAgentsPage{basePage, agentItems, timeRange.Range}
return renderTemplate("panel_analytics_agents", w, r, user, &pi)
}
func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, system").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
osMap, err := analyticsRowsToNameMap(rows)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Sort this slice
var systemItems []common.PanelAnalyticsAgentsItem
for system, count := range osMap {
sSystem, ok := phrases.GetOSPhrase(system)
if !ok {
sSystem = system
}
systemItems = append(systemItems, common.PanelAnalyticsAgentsItem{
Agent: system,
FriendlyAgent: sSystem,
Count: count,
})
}
pi := common.PanelAnalyticsAgentsPage{basePage, systemItems, timeRange.Range}
return renderTemplate("panel_analytics_systems", w, r, user, &pi)
}
func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, lang").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
langMap, err := analyticsRowsToNameMap(rows)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Can we de-duplicate these analytics functions further?
// TODO: Sort this slice
var langItems []common.PanelAnalyticsAgentsItem
for lang, count := range langMap {
lLang, ok := phrases.GetHumanLangPhrase(lang)
if !ok {
lLang = lang
}
langItems = append(langItems, common.PanelAnalyticsAgentsItem{
Agent: lang,
FriendlyAgent: lLang,
Count: count,
})
}
pi := common.PanelAnalyticsAgentsPage{basePage, langItems, timeRange.Range}
return renderTemplate("panel_analytics_langs", w, r, user, &pi)
}
func AnalyticsReferrers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil {
return ferr
}
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
rows, err := qgen.NewAcc().Select("viewchunks_referrers").Columns("count, domain").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r)
}
refMap, err := analyticsRowsToNameMap(rows)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Sort this slice
var refItems []common.PanelAnalyticsAgentsItem
for domain, count := range refMap {
refItems = append(refItems, common.PanelAnalyticsAgentsItem{
Agent: common.SanitiseSingleLine(domain),
Count: count,
})
}
pi := common.PanelAnalyticsAgentsPage{basePage, refItems, timeRange.Range}
return renderTemplate("panel_analytics_referrers", w, r, user, &pi)
}