github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/panel/analytics.go (about) 1 package panel 2 3 import ( 4 "database/sql" 5 "errors" 6 "log" 7 "net/http" 8 "strconv" 9 "strings" 10 "time" 11 12 c "github.com/Azareal/Gosora/common" 13 p "github.com/Azareal/Gosora/common/phrases" 14 qgen "github.com/Azareal/Gosora/query_gen" 15 ) 16 17 func analyticsTimeRange(rawTimeRange string) (*c.AnalyticsTimeRange, error) { 18 tr := &c.AnalyticsTimeRange{ 19 Quantity: 6, 20 Unit: "hour", 21 Slices: 12, 22 SliceWidth: 60 * 30, 23 Range: "six-hours", 24 } 25 26 switch rawTimeRange { 27 // This might be pushing it, we might want to come up with a more efficient scheme for dealing with large timeframes like this 28 case "one-year": 29 tr.Quantity = 12 30 tr.Unit = "month" 31 tr.Slices = 12 32 tr.SliceWidth = 60 * 60 * 24 * 30 33 case "three-months": 34 tr.Quantity = 90 35 tr.Unit = "day" 36 tr.Slices = 30 37 tr.SliceWidth = 60 * 60 * 24 * 3 38 case "one-month": 39 tr.Quantity = 30 40 tr.Unit = "day" 41 tr.Slices = 30 42 tr.SliceWidth = 60 * 60 * 24 43 case "one-week": 44 tr.Quantity = 7 45 tr.Unit = "day" 46 tr.Slices = 14 47 tr.SliceWidth = 60 * 60 * 12 48 case "two-days": // Two days is experimental 49 tr.Quantity = 2 50 tr.Unit = "day" 51 tr.Slices = 24 52 tr.SliceWidth = 60 * 60 * 2 53 case "one-day": 54 tr.Quantity = 1 55 tr.Unit = "day" 56 tr.Slices = 24 57 tr.SliceWidth = 60 * 60 58 case "twelve-hours": 59 tr.Quantity = 12 60 tr.Slices = 24 61 case "six-hours", "": 62 return tr, nil 63 default: 64 return tr, errors.New("Unknown time range") 65 } 66 tr.Range = rawTimeRange 67 return tr, nil 68 } 69 70 type pAvg struct { 71 Avg int64 72 Tot int64 73 } 74 75 func analyticsRowsToAverageMap(rows *sql.Rows, labelList []int64, avgMap map[int64]int64) (map[int64]int64, error) { 76 defer rows.Close() 77 for rows.Next() { 78 var count int64 79 var createdAt time.Time 80 e := rows.Scan(&count, &createdAt) 81 if e != nil { 82 return avgMap, e 83 } 84 unixCreatedAt := createdAt.Unix() 85 // TODO: Bulk log this 86 if c.Dev.SuperDebug { 87 log.Print("count: ", count) 88 log.Print("createdAt: ", createdAt, " - ", unixCreatedAt) 89 } 90 pAvgMap := make(map[int64]pAvg) 91 for _, value := range labelList { 92 if unixCreatedAt > value { 93 prev := pAvgMap[value] 94 prev.Avg += count 95 prev.Tot++ 96 pAvgMap[value] = prev 97 break 98 } 99 } 100 for key, pAvg := range pAvgMap { 101 avgMap[key] = pAvg.Avg / pAvg.Tot 102 } 103 } 104 return avgMap, rows.Err() 105 } 106 107 func analyticsRowsToAverageMap2(rows *sql.Rows, labelList []int64, avgMap map[int64]int64, typ int) (map[int64]int64, error) { 108 defer rows.Close() 109 for rows.Next() { 110 var stack, heap int64 111 var createdAt time.Time 112 e := rows.Scan(&stack, &heap, &createdAt) 113 if e != nil { 114 return avgMap, e 115 } 116 unixCreatedAt := createdAt.Unix() 117 // TODO: Bulk log this 118 if c.Dev.SuperDebug { 119 log.Print("stack: ", stack) 120 log.Print("heap: ", heap) 121 log.Print("createdAt: ", createdAt, " - ", unixCreatedAt) 122 } 123 if typ == 1 { 124 heap = 0 125 } else if typ == 2 { 126 stack = 0 127 } 128 pAvgMap := make(map[int64]pAvg) 129 for _, value := range labelList { 130 if unixCreatedAt > value { 131 prev := pAvgMap[value] 132 prev.Avg += stack + heap 133 prev.Tot++ 134 pAvgMap[value] = prev 135 break 136 } 137 } 138 for key, pAvg := range pAvgMap { 139 avgMap[key] = pAvg.Avg / pAvg.Tot 140 } 141 } 142 return avgMap, rows.Err() 143 } 144 145 func analyticsRowsToAverageMap3(rows *sql.Rows, labelList []int64, avgMap map[int64]int64, typ int) (map[int64]int64, error) { 146 defer rows.Close() 147 for rows.Next() { 148 var low, high, avg int64 149 var createdAt time.Time 150 e := rows.Scan(&low, &high, &avg, &createdAt) 151 if e != nil { 152 return avgMap, e 153 } 154 unixCreatedAt := createdAt.Unix() 155 // TODO: Bulk log this 156 if c.Dev.SuperDebug { 157 log.Print("low: ", low) 158 log.Print("high: ", high) 159 log.Print("avg: ", avg) 160 log.Print("createdAt: ", createdAt, " - ", unixCreatedAt) 161 } 162 var dat int64 163 switch typ { 164 case 0: 165 dat = low 166 case 1: 167 dat = high 168 default: 169 dat = avg 170 } 171 pAvgMap := make(map[int64]pAvg) 172 for _, val := range labelList { 173 if unixCreatedAt > val { 174 prev := pAvgMap[val] 175 prev.Avg += dat 176 prev.Tot++ 177 pAvgMap[val] = prev 178 break 179 } 180 } 181 for key, pAvg := range pAvgMap { 182 avgMap[key] = pAvg.Avg / pAvg.Tot 183 } 184 } 185 return avgMap, rows.Err() 186 } 187 188 func PreAnalyticsDetail(w http.ResponseWriter, r *http.Request, u *c.User) (*c.BasePanelPage, c.RouteError) { 189 bp, fe := buildBasePage(w, r, u, "analytics", "analytics") 190 if fe != nil { 191 return nil, fe 192 } 193 bp.AddSheet("chartist/chartist.min.css") 194 bp.AddScript("chartist/chartist.min.js") 195 bp.AddScriptAsync("analytics.js") 196 bp.LooseCSP = true 197 return bp, nil 198 } 199 200 func createTimeGraph(series [][]int64, labelList []int64, legends ...[]string) c.PanelTimeGraph { 201 var llegends []string 202 if len(legends) > 0 { 203 llegends = legends[0] 204 } 205 graph := c.PanelTimeGraph{Series: series, Labels: labelList, Legends: llegends} 206 c.DebugLogf("graph: %+v\n", graph) 207 return graph 208 } 209 210 func CreateViewListItems(revLabelList []int64, viewMap map[int64]int64) ([]int64, []c.PanelAnalyticsItem) { 211 viewList := make([]int64, len(revLabelList)) 212 viewItems := make([]c.PanelAnalyticsItem, len(revLabelList)) 213 for i, val := range revLabelList { 214 viewList[i] = viewMap[val] 215 viewItems[i] = c.PanelAnalyticsItem{Time: val, Count: viewMap[val]} 216 } 217 return viewList, viewItems 218 } 219 220 func AnalyticsViews(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 221 bp, fe := PreAnalyticsDetail(w, r, u) 222 if fe != nil { 223 return fe 224 } 225 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 226 if e != nil { 227 return c.LocalError(e.Error(), w, r, u) 228 } 229 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 230 231 c.DebugLog("in panel.AnalyticsViews") 232 // TODO: Add some sort of analytics store / iterator? 233 viewMap, e = c.Analytics.FillViewMap("viewchunks", tr, labelList, viewMap, "route", "") 234 if e != nil { 235 return c.InternalError(e, w, r) 236 } 237 viewList, viewItems := CreateViewListItems(revLabelList, viewMap) 238 239 graph := createTimeGraph([][]int64{viewList}, labelList) 240 var ttime string 241 if tr.Range == "six-hours" || tr.Range == "twelve-hours" || tr.Range == "one-day" { 242 ttime = "time" 243 } 244 245 pi := c.PanelAnalyticsStd{graph, viewItems, tr.Range, tr.Unit, ttime} 246 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_views", pi}) 247 } 248 249 func AnalyticsRouteViews(w http.ResponseWriter, r *http.Request, u *c.User, route string) c.RouteError { 250 bp, fe := PreAnalyticsDetail(w, r, u) 251 if fe != nil { 252 return fe 253 } 254 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 255 if e != nil { 256 return c.LocalError(e.Error(), w, r, u) 257 } 258 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 259 260 c.DebugLog("in panel.AnalyticsRouteViews") 261 // TODO: Validate the route is valid 262 viewMap, e = c.Analytics.FillViewMap("viewchunks", tr, labelList, viewMap, "route", route) 263 if e != nil { 264 return c.InternalError(e, w, r) 265 } 266 viewList, viewItems := CreateViewListItems(revLabelList, viewMap) 267 graph := createTimeGraph([][]int64{viewList}, labelList) 268 269 pi := c.PanelAnalyticsRoutePage{bp, c.SanitiseSingleLine(route), graph, viewItems, tr.Range} 270 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_route_views", pi}) 271 } 272 273 func AnalyticsAgentViews(w http.ResponseWriter, r *http.Request, u *c.User, agent string) c.RouteError { 274 bp, ferr := PreAnalyticsDetail(w, r, u) 275 if ferr != nil { 276 return ferr 277 } 278 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 279 if e != nil { 280 return c.LocalError(e.Error(), w, r, u) 281 } 282 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 283 // ? 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 284 agent = c.SanitiseSingleLine(agent) 285 286 c.DebugLog("in panel.AnalyticsAgentViews") 287 // TODO: Verify the agent is valid 288 viewMap, e = c.Analytics.FillViewMap("viewchunks_agents", tr, labelList, viewMap, "browser", agent) 289 if e != nil { 290 return c.InternalError(e, w, r) 291 } 292 viewList := CreateViewList(revLabelList, viewMap) 293 graph := createTimeGraph([][]int64{viewList}, labelList) 294 295 friendlyAgent, ok := p.GetUserAgentPhrase(agent) 296 if !ok { 297 friendlyAgent = agent 298 } 299 300 pi := c.PanelAnalyticsAgentPage{bp, agent, friendlyAgent, graph, tr.Range} 301 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_agent_views", pi}) 302 } 303 304 func AnalyticsForumViews(w http.ResponseWriter, r *http.Request, u *c.User, sfid string) c.RouteError { 305 bp, ferr := PreAnalyticsDetail(w, r, u) 306 if ferr != nil { 307 return ferr 308 } 309 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 310 if e != nil { 311 return c.LocalError(e.Error(), w, r, u) 312 } 313 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 314 315 fid, e := strconv.Atoi(sfid) 316 if e != nil { 317 return c.LocalError("Invalid integer", w, r, u) 318 } 319 320 c.DebugLog("in panel.AnalyticsForumViews") 321 // TODO: Verify the agent is valid 322 viewMap, e = c.Analytics.FillViewMap("viewchunks_forums", tr, labelList, viewMap, "forum", fid) 323 if e != nil { 324 return c.InternalError(e, w, r) 325 } 326 viewList := CreateViewList(revLabelList, viewMap) 327 graph := createTimeGraph([][]int64{viewList}, labelList) 328 329 forum, e := c.Forums.Get(fid) 330 if e != nil { 331 return c.InternalError(e, w, r) 332 } 333 334 pi := c.PanelAnalyticsAgentPage{bp, sfid, forum.Name, graph, tr.Range} 335 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_forum_views", pi}) 336 } 337 338 func AnalyticsSystemViews(w http.ResponseWriter, r *http.Request, u *c.User, system string) c.RouteError { 339 bp, ferr := PreAnalyticsDetail(w, r, u) 340 if ferr != nil { 341 return ferr 342 } 343 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 344 if e != nil { 345 return c.LocalError(e.Error(), w, r, u) 346 } 347 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 348 system = c.SanitiseSingleLine(system) 349 350 c.DebugLog("in panel.AnalyticsSystemViews") 351 // TODO: Verify the OS name is valid 352 viewMap, e = c.Analytics.FillViewMap("viewchunks_systems", tr, labelList, viewMap, "system", system) 353 if e != nil { 354 return c.InternalError(e, w, r) 355 } 356 viewList := CreateViewList(revLabelList, viewMap) 357 graph := createTimeGraph([][]int64{viewList}, labelList) 358 359 friendlySystem, ok := p.GetOSPhrase(system) 360 if !ok { 361 friendlySystem = system 362 } 363 364 pi := c.PanelAnalyticsAgentPage{bp, system, friendlySystem, graph, tr.Range} 365 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_system_views", pi}) 366 } 367 368 func CreateViewList(revLabelList []int64, viewMap map[int64]int64) []int64 { 369 viewList := make([]int64, len(revLabelList)) 370 for i, val := range revLabelList { 371 viewList[i] = viewMap[val] 372 } 373 return viewList 374 } 375 376 func AnalyticsLanguageViews(w http.ResponseWriter, r *http.Request, u *c.User, lang string) c.RouteError { 377 bp, ferr := PreAnalyticsDetail(w, r, u) 378 if ferr != nil { 379 return ferr 380 } 381 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 382 if e != nil { 383 return c.LocalError(e.Error(), w, r, u) 384 } 385 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 386 lang = c.SanitiseSingleLine(lang) 387 388 c.DebugLog("in panel.AnalyticsLanguageViews") 389 // TODO: Verify the language code is valid 390 viewMap, e = c.Analytics.FillViewMap("viewchunks_langs", tr, labelList, viewMap, "lang", lang) 391 if e != nil { 392 return c.InternalError(e, w, r) 393 } 394 viewList := CreateViewList(revLabelList, viewMap) 395 graph := createTimeGraph([][]int64{viewList}, labelList) 396 397 friendlyLang, ok := p.GetHumanLangPhrase(lang) 398 if !ok { 399 friendlyLang = lang 400 } 401 402 pi := c.PanelAnalyticsAgentPage{bp, lang, friendlyLang, graph, tr.Range} 403 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_lang_views", pi}) 404 } 405 406 func AnalyticsReferrerViews(w http.ResponseWriter, r *http.Request, u *c.User, domain string) c.RouteError { 407 bp, ferr := PreAnalyticsDetail(w, r, u) 408 if ferr != nil { 409 return ferr 410 } 411 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 412 if e != nil { 413 return c.LocalError(e.Error(), w, r, u) 414 } 415 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 416 417 c.DebugLog("in panel.AnalyticsReferrerViews") 418 // TODO: Verify the agent is valid 419 viewMap, e = c.Analytics.FillViewMap("viewchunks_referrers", tr, labelList, viewMap, "domain", domain) 420 if e != nil { 421 return c.InternalError(e, w, r) 422 } 423 viewList := CreateViewList(revLabelList, viewMap) 424 graph := createTimeGraph([][]int64{viewList}, labelList) 425 426 pi := c.PanelAnalyticsAgentPage{bp, c.SanitiseSingleLine(domain), "", graph, tr.Range} 427 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_referrer_views", pi}) 428 } 429 430 func AnalyticsTopics(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 431 bp, ferr := PreAnalyticsDetail(w, r, u) 432 if ferr != nil { 433 return ferr 434 } 435 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 436 if e != nil { 437 return c.LocalError(e.Error(), w, r, u) 438 } 439 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 440 441 c.DebugLog("in panel.AnalyticsTopics") 442 viewMap, e = c.Analytics.FillViewMap("topicchunks", tr, labelList, viewMap, "") 443 if e != nil { 444 return c.InternalError(e, w, r) 445 } 446 viewList, viewItems := CreateViewListItems(revLabelList, viewMap) 447 graph := createTimeGraph([][]int64{viewList}, labelList) 448 449 pi := c.PanelAnalyticsStd{graph, viewItems, tr.Range, tr.Unit, "time"} 450 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_topics", pi}) 451 } 452 453 func AnalyticsPosts(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 454 bp, fe := PreAnalyticsDetail(w, r, u) 455 if fe != nil { 456 return fe 457 } 458 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 459 if e != nil { 460 return c.LocalError(e.Error(), w, r, u) 461 } 462 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 463 464 c.DebugLog("in panel.AnalyticsPosts") 465 viewMap, e = c.Analytics.FillViewMap("postchunks", tr, labelList, viewMap, "") 466 if e != nil { 467 return c.InternalError(e, w, r) 468 } 469 viewList, viewItems := CreateViewListItems(revLabelList, viewMap) 470 graph := createTimeGraph([][]int64{viewList}, labelList) 471 472 pi := c.PanelAnalyticsStd{graph, viewItems, tr.Range, tr.Unit, "time"} 473 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_posts", pi}) 474 } 475 476 func AnalyticsMemory(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 477 bp, fe := PreAnalyticsDetail(w, r, u) 478 if fe != nil { 479 return fe 480 } 481 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 482 if e != nil { 483 return c.LocalError(e.Error(), w, r, u) 484 } 485 revLabelList, labelList, avgMap := c.AnalyticsTimeRangeToLabelList(tr) 486 487 c.DebugLog("in panel.AnalyticsMemory") 488 rows, e := qgen.NewAcc().Select("memchunks").Columns("count,createdAt").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 489 if e != nil && e != sql.ErrNoRows { 490 return c.InternalError(e, w, r) 491 } 492 avgMap, e = analyticsRowsToAverageMap(rows, labelList, avgMap) 493 if e != nil { 494 return c.InternalError(e, w, r) 495 } 496 497 // TODO: Adjust for the missing chunks in week and month 498 avgList := make([]int64, len(revLabelList)) 499 avgItems := make([]c.PanelAnalyticsItemUnit, len(revLabelList)) 500 for i, value := range revLabelList { 501 avgList[i] = avgMap[value] 502 cv, cu := c.ConvertByteUnit(float64(avgMap[value])) 503 avgItems[i] = c.PanelAnalyticsItemUnit{Time: value, Unit: cu, Count: int64(cv)} 504 } 505 graph := createTimeGraph([][]int64{avgList}, labelList) 506 507 pi := c.PanelAnalyticsStdUnit{graph, avgItems, tr.Range, tr.Unit, "time"} 508 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_memory", pi}) 509 } 510 511 // TODO: Show stack and heap memory separately on the chart 512 func AnalyticsActiveMemory(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 513 bp, ferr := PreAnalyticsDetail(w, r, u) 514 if ferr != nil { 515 return ferr 516 } 517 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 518 if e != nil { 519 return c.LocalError(e.Error(), w, r, u) 520 } 521 revLabelList, labelList, avgMap := c.AnalyticsTimeRangeToLabelList(tr) 522 523 c.DebugLog("in panel.AnalyticsActiveMemory") 524 rows, e := qgen.NewAcc().Select("memchunks").Columns("stack,heap,createdAt").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 525 if e != nil && e != sql.ErrNoRows { 526 return c.InternalError(e, w, r) 527 } 528 529 var typ int 530 switch r.FormValue("mtype") { 531 case "1": 532 typ = 1 533 case "2": 534 typ = 2 535 default: 536 typ = 0 537 } 538 avgMap, e = analyticsRowsToAverageMap2(rows, labelList, avgMap, typ) 539 if e != nil { 540 return c.InternalError(e, w, r) 541 } 542 543 // TODO: Adjust for the missing chunks in week and month 544 avgList := make([]int64, len(revLabelList)) 545 avgItems := make([]c.PanelAnalyticsItemUnit, len(revLabelList)) 546 for i, value := range revLabelList { 547 avgList[i] = avgMap[value] 548 cv, cu := c.ConvertByteUnit(float64(avgMap[value])) 549 avgItems[i] = c.PanelAnalyticsItemUnit{Time: value, Unit: cu, Count: int64(cv)} 550 } 551 graph := createTimeGraph([][]int64{avgList}, labelList) 552 553 pi := c.PanelAnalyticsActiveMemory{graph, avgItems, tr.Range, tr.Unit, "time", typ} 554 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_active_memory", pi}) 555 } 556 557 func AnalyticsPerf(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 558 bp, ferr := PreAnalyticsDetail(w, r, u) 559 if ferr != nil { 560 return ferr 561 } 562 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 563 if e != nil { 564 return c.LocalError(e.Error(), w, r, u) 565 } 566 revLabelList, labelList, avgMap := c.AnalyticsTimeRangeToLabelList(tr) 567 568 c.DebugLog("in panel.AnalyticsPerf") 569 rows, e := qgen.NewAcc().Select("perfchunks").Columns("low,high,avg,createdAt").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 570 if e != nil && e != sql.ErrNoRows { 571 return c.InternalError(e, w, r) 572 } 573 574 var typ int 575 switch r.FormValue("type") { 576 case "0": 577 typ = 0 578 case "1": 579 typ = 1 580 default: 581 typ = 2 582 } 583 avgMap, e = analyticsRowsToAverageMap3(rows, labelList, avgMap, typ) 584 if e != nil { 585 return c.InternalError(e, w, r) 586 } 587 588 // TODO: Adjust for the missing chunks in week and month 589 avgList := make([]int64, len(revLabelList)) 590 avgItems := make([]c.PanelAnalyticsItemUnit, len(revLabelList)) 591 for i, value := range revLabelList { 592 avgList[i] = avgMap[value] 593 cv, cu := c.ConvertPerfUnit(float64(avgMap[value])) 594 avgItems[i] = c.PanelAnalyticsItemUnit{Time: value, Unit: cu, Count: int64(cv)} 595 } 596 graph := createTimeGraph([][]int64{avgList}, labelList) 597 598 pi := c.PanelAnalyticsPerf{graph, avgItems, tr.Range, tr.Unit, "time", typ} 599 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_performance", pi}) 600 } 601 602 func analyticsRowsToAvgDuoMap(rows *sql.Rows, labelList []int64, avgMap map[int64]int64) (map[string]map[int64]int64, map[string]int, error) { 603 aMap := make(map[string]map[int64]int64) 604 nameMap := make(map[string]int) 605 defer rows.Close() 606 for rows.Next() { 607 var count int64 608 var name string 609 var createdAt time.Time 610 e := rows.Scan(&count, &name, &createdAt) 611 if e != nil { 612 return aMap, nameMap, e 613 } 614 615 // TODO: Bulk log this 616 unixCreatedAt := createdAt.Unix() 617 if c.Dev.SuperDebug { 618 log.Print("count: ", count) 619 log.Print("name: ", name) 620 log.Print("createdAt: ", createdAt, " - ", unixCreatedAt) 621 } 622 623 vvMap, ok := aMap[name] 624 if !ok { 625 vvMap = make(map[int64]int64) 626 for key, val := range avgMap { 627 vvMap[key] = val 628 } 629 aMap[name] = vvMap 630 } 631 for _, value := range labelList { 632 if unixCreatedAt > value { 633 vvMap[value] = (vvMap[value] + count) / 2 634 break 635 } 636 } 637 nameMap[name] = (nameMap[name] + int(count)) / 2 638 } 639 return aMap, nameMap, rows.Err() 640 } 641 642 func sortOVList(ovList []OVItem) []OVItem { 643 // Use bubble sort for now as there shouldn't be too many items 644 for i := 0; i < len(ovList)-1; i++ { 645 for j := 0; j < len(ovList)-1; j++ { 646 if ovList[j].count > ovList[j+1].count { 647 temp := ovList[j] 648 ovList[j] = ovList[j+1] 649 ovList[j+1] = temp 650 } 651 } 652 } 653 654 // Invert the direction 655 tOVList := make([]OVItem, len(ovList)) 656 for i, ii := len(ovList)-1, 0; i >= 0; i-- { 657 tOVList[ii] = ovList[i] 658 ii++ 659 } 660 return tOVList 661 } 662 663 func analyticsAMapToOVList(aMap map[string]map[int64]int64) []OVItem { 664 // Order the map 665 ovList, i := make([]OVItem, len(aMap)), 0 666 for name, avgMap := range aMap { 667 var totcount int 668 for _, count := range avgMap { 669 totcount = (totcount + int(count)) / 2 670 } 671 ovList[i] = OVItem{name, totcount, avgMap} 672 i++ 673 } 674 return sortOVList(ovList) 675 } 676 677 func AnalyticsRoutesPerf(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 678 bp, ferr := PreAnalyticsDetail(w, r, u) 679 if ferr != nil { 680 return ferr 681 } 682 bp.AddScript("chartist/chartist-plugin-legend.min.js") 683 bp.AddSheet("chartist/chartist-plugin-legend.css") 684 685 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 686 if e != nil { 687 return c.LocalError(e.Error(), w, r, u) 688 } 689 // avgMap contains timestamps but not the averages for those stamps 690 revLabelList, labelList, avgMap := c.AnalyticsTimeRangeToLabelList(tr) 691 692 rows, e := qgen.NewAcc().Select("viewchunks").Columns("avg,route,createdAt").Where("count!=0 AND route!=''").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 693 if e != nil && e != sql.ErrNoRows { 694 return c.InternalError(e, w, r) 695 } 696 aMap, routeMap, e := analyticsRowsToAvgDuoMap(rows, labelList, avgMap) 697 if e != nil { 698 return c.InternalError(e, w, r) 699 } 700 //c.DebugLogf("aMap: %+v\n", aMap) 701 //c.DebugLogf("routeMap: %+v\n", routeMap) 702 ovList := analyticsAMapToOVList(aMap) 703 //c.DebugLogf("ovList: %+v\n", ovList) 704 705 ex := strings.Split(r.FormValue("ex"), ",") 706 inEx := func(name string) bool { 707 for _, e := range ex { 708 if e == name { 709 return true 710 } 711 } 712 return false 713 } 714 715 var vList [][]int64 716 var legendList []string 717 var i int 718 for _, ovitem := range ovList { 719 if inEx(ovitem.name) { 720 continue 721 } 722 if strings.HasPrefix(ovitem.name, "panel.") { 723 continue 724 } 725 viewList := make([]int64, len(revLabelList)) 726 for i, val := range revLabelList { 727 viewList[i] = ovitem.viewMap[val] 728 } 729 vList = append(vList, viewList) 730 shortName := strings.Replace(ovitem.name, "routes.", "r.", -1) 731 legendList = append(legendList, shortName) 732 if i >= 7 { 733 break 734 } 735 i++ 736 } 737 graph := createTimeGraph(vList, labelList, legendList) 738 739 // TODO: Sort this slice 740 var routeItems []c.PanelAnalyticsRoutesPerfItem 741 for route, count := range routeMap { 742 if inEx(route) { 743 continue 744 } 745 cv, cu := c.ConvertPerfUnit(float64(count)) 746 routeItems = append(routeItems, c.PanelAnalyticsRoutesPerfItem{ 747 Route: route, 748 Unit: cu, 749 Count: int(cv), 750 }) 751 } 752 753 pi := c.PanelAnalyticsRoutesPerfPage{bp, routeItems, graph, tr.Range} 754 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_routes_perf", pi}) 755 } 756 757 func analyticsRowsToRefMap(rows *sql.Rows) (map[string]int, error) { 758 nameMap := make(map[string]int) 759 defer rows.Close() 760 c.DebugDetail("name - count") 761 for rows.Next() { 762 var count int 763 var name string 764 e := rows.Scan(&count, &name) 765 if e != nil { 766 return nameMap, e 767 } 768 // TODO: Bulk log this 769 if c.Dev.SuperDebug { 770 log.Print(name, " - ", count) 771 } 772 nameMap[name] += count 773 } 774 return nameMap, rows.Err() 775 } 776 777 func analyticsRowsToDuoMap(rows *sql.Rows, labelList []int64, viewMap map[int64]int64) (map[string]map[int64]int64, map[string]int, error) { 778 vMap := make(map[string]map[int64]int64) 779 nameMap := make(map[string]int) 780 defer rows.Close() 781 for rows.Next() { 782 var count int64 783 var name string 784 var createdAt time.Time 785 e := rows.Scan(&count, &name, &createdAt) 786 if e != nil { 787 return vMap, nameMap, e 788 } 789 790 // TODO: Bulk log this 791 unixCreatedAt := createdAt.Unix() 792 if c.Dev.SuperDebug { 793 log.Print("count: ", count) 794 log.Print("name: ", name) 795 log.Print("createdAt: ", createdAt, " - ", unixCreatedAt) 796 } 797 798 vvMap, ok := vMap[name] 799 if !ok { 800 vvMap = make(map[int64]int64) 801 for key, val := range viewMap { 802 vvMap[key] = val 803 } 804 vMap[name] = vvMap 805 } 806 for _, value := range labelList { 807 if unixCreatedAt > value { 808 vvMap[value] += count 809 break 810 } 811 } 812 nameMap[name] += int(count) 813 } 814 return vMap, nameMap, rows.Err() 815 } 816 817 type OVItem struct { 818 name string 819 count int 820 viewMap map[int64]int64 821 } 822 823 func analyticsVMapToOVList(vMap map[string]map[int64]int64) (ovList []OVItem) { 824 // Order the map 825 ovList, i := make([]OVItem, len(vMap)), 0 826 for name, viewMap := range vMap { 827 var totcount int 828 for _, count := range viewMap { 829 totcount += int(count) 830 } 831 ovList[i] = OVItem{name, totcount, viewMap} 832 i++ 833 } 834 return sortOVList(ovList) 835 } 836 837 func AnalyticsForums(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 838 bp, ferr := PreAnalyticsDetail(w, r, u) 839 if ferr != nil { 840 return ferr 841 } 842 bp.AddScript("chartist/chartist-plugin-legend.min.js") 843 bp.AddSheet("chartist/chartist-plugin-legend.css") 844 845 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 846 if e != nil { 847 return c.LocalError(e.Error(), w, r, u) 848 } 849 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 850 851 rows, e := qgen.NewAcc().Select("viewchunks_forums").Columns("count,forum,createdAt").Where("forum!=''").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 852 if e != nil && e != sql.ErrNoRows { 853 return c.InternalError(e, w, r) 854 } 855 vMap, forumMap, e := analyticsRowsToDuoMap(rows, labelList, viewMap) 856 if e != nil { 857 return c.InternalError(e, w, r) 858 } 859 ovList := analyticsVMapToOVList(vMap) 860 861 var vList [][]int64 862 var legendList []string 863 var i int 864 for _, ovitem := range ovList { 865 viewList := make([]int64, len(revLabelList)) 866 for i, val := range revLabelList { 867 viewList[i] = ovitem.viewMap[val] 868 } 869 vList = append(vList, viewList) 870 fid, e := strconv.Atoi(ovitem.name) 871 if e != nil { 872 return c.InternalError(e, w, r) 873 } 874 var lName string 875 forum, e := c.Forums.Get(fid) 876 if e == sql.ErrNoRows { 877 lName = "Deleted Forum" // TODO: Localise this 878 } else if e != nil { 879 return c.InternalError(e, w, r) 880 } else { 881 lName = forum.Name 882 } 883 legendList = append(legendList, lName) 884 if i >= 6 { 885 break 886 } 887 i++ 888 } 889 graph := createTimeGraph(vList, labelList, legendList) 890 891 // TODO: Sort this slice 892 forumItems, i := make([]c.PanelAnalyticsAgentsItem, len(forumMap)), 0 893 for sfid, count := range forumMap { 894 fid, e := strconv.Atoi(sfid) 895 if e != nil { 896 return c.InternalError(e, w, r) 897 } 898 var lName string 899 forum, e := c.Forums.Get(fid) 900 if e == sql.ErrNoRows { 901 // TODO: Localise this 902 lName = "Deleted Forum" 903 } else if e != nil { 904 return c.InternalError(e, w, r) 905 } else { 906 lName = forum.Name 907 } 908 forumItems[i] = c.PanelAnalyticsAgentsItem{ 909 Agent: sfid, 910 FriendlyAgent: lName, 911 Count: count, 912 } 913 i++ 914 } 915 916 pi := c.PanelAnalyticsDuoPage{bp, forumItems, graph, tr.Range} 917 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_forums", pi}) 918 } 919 920 func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 921 bp, ferr := PreAnalyticsDetail(w, r, u) 922 if ferr != nil { 923 return ferr 924 } 925 bp.AddScript("chartist/chartist-plugin-legend.min.js") 926 bp.AddSheet("chartist/chartist-plugin-legend.css") 927 928 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 929 if e != nil { 930 return c.LocalError(e.Error(), w, r, u) 931 } 932 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 933 934 rows, e := qgen.NewAcc().Select("viewchunks").Columns("count,route,createdAt").Where("route!=''").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 935 if e != nil && e != sql.ErrNoRows { 936 return c.InternalError(e, w, r) 937 } 938 vMap, routeMap, e := analyticsRowsToDuoMap(rows, labelList, viewMap) 939 if e != nil { 940 return c.InternalError(e, w, r) 941 } 942 //c.DebugLogf("vMap: %+v\n", vMap) 943 //c.DebugLogf("routeMap: %+v\n", routeMap) 944 ovList := analyticsVMapToOVList(vMap) 945 //c.DebugLogf("ovList: %+v\n", ovList) 946 947 ex := strings.Split(r.FormValue("ex"), ",") 948 inEx := func(name string) bool { 949 for _, e := range ex { 950 if e == name { 951 return true 952 } 953 } 954 return false 955 } 956 957 var vList [][]int64 958 var legendList []string 959 var i int 960 for _, ovitem := range ovList { 961 if inEx(ovitem.name) { 962 continue 963 } 964 viewList := make([]int64, len(revLabelList)) 965 for i, val := range revLabelList { 966 viewList[i] = ovitem.viewMap[val] 967 } 968 vList = append(vList, viewList) 969 shortName := strings.Replace(ovitem.name, "routes.", "r.", -1) 970 legendList = append(legendList, shortName) 971 if i >= 7 { 972 break 973 } 974 i++ 975 } 976 graph := createTimeGraph(vList, labelList, legendList) 977 978 // TODO: Sort this slice 979 var routeItems []c.PanelAnalyticsRoutesItem 980 for route, count := range routeMap { 981 if inEx(route) { 982 continue 983 } 984 routeItems = append(routeItems, c.PanelAnalyticsRoutesItem{ 985 Route: route, 986 Count: count, 987 }) 988 } 989 990 pi := c.PanelAnalyticsRoutesPage{bp, routeItems, graph, tr.Range} 991 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_routes", pi}) 992 } 993 994 // Trialling multi-series charts 995 func AnalyticsAgents(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 996 bp, ferr := PreAnalyticsDetail(w, r, u) 997 if ferr != nil { 998 return ferr 999 } 1000 bp.AddScript("chartist/chartist-plugin-legend.min.js") 1001 bp.AddSheet("chartist/chartist-plugin-legend.css") 1002 1003 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 1004 if e != nil { 1005 return c.LocalError(e.Error(), w, r, u) 1006 } 1007 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 1008 1009 rows, e := qgen.NewAcc().Select("viewchunks_agents").Columns("count,browser,createdAt").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 1010 if e != nil && e != sql.ErrNoRows { 1011 return c.InternalError(e, w, r) 1012 } 1013 vMap, agentMap, e := analyticsRowsToDuoMap(rows, labelList, viewMap) 1014 if e != nil { 1015 return c.InternalError(e, w, r) 1016 } 1017 ovList := analyticsVMapToOVList(vMap) 1018 1019 ex := strings.Split(r.FormValue("ex"), ",") 1020 inEx := func(name string) bool { 1021 for _, e := range ex { 1022 if e == name { 1023 return true 1024 } 1025 } 1026 return false 1027 } 1028 1029 var vList [][]int64 1030 var legendList []string 1031 var i int 1032 for _, ovitem := range ovList { 1033 if inEx(ovitem.name) { 1034 continue 1035 } 1036 lName, ok := p.GetUserAgentPhrase(ovitem.name) 1037 if !ok { 1038 lName = ovitem.name 1039 } 1040 if inEx(lName) { 1041 continue 1042 } 1043 viewList := make([]int64, len(revLabelList)) 1044 for i, val := range revLabelList { 1045 viewList[i] = ovitem.viewMap[val] 1046 } 1047 vList = append(vList, viewList) 1048 legendList = append(legendList, lName) 1049 if i >= 7 { 1050 break 1051 } 1052 i++ 1053 } 1054 graph := createTimeGraph(vList, labelList, legendList) 1055 1056 // TODO: Sort this slice 1057 var agentItems []c.PanelAnalyticsAgentsItem 1058 for agent, count := range agentMap { 1059 if inEx(agent) { 1060 continue 1061 } 1062 aAgent, ok := p.GetUserAgentPhrase(agent) 1063 if !ok { 1064 aAgent = agent 1065 } 1066 if inEx(aAgent) { 1067 continue 1068 } 1069 agentItems = append(agentItems, c.PanelAnalyticsAgentsItem{ 1070 Agent: agent, 1071 FriendlyAgent: aAgent, 1072 Count: count, 1073 }) 1074 } 1075 1076 pi := c.PanelAnalyticsDuoPage{bp, agentItems, graph, tr.Range} 1077 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_agents", pi}) 1078 } 1079 1080 func AnalyticsSystems(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 1081 bp, ferr := PreAnalyticsDetail(w, r, u) 1082 if ferr != nil { 1083 return ferr 1084 } 1085 bp.AddScript("chartist/chartist-plugin-legend.min.js") 1086 bp.AddSheet("chartist/chartist-plugin-legend.css") 1087 1088 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 1089 if e != nil { 1090 return c.LocalError(e.Error(), w, r, u) 1091 } 1092 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 1093 1094 rows, e := qgen.NewAcc().Select("viewchunks_systems").Columns("count,system,createdAt").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 1095 if e != nil && e != sql.ErrNoRows { 1096 return c.InternalError(e, w, r) 1097 } 1098 vMap, osMap, e := analyticsRowsToDuoMap(rows, labelList, viewMap) 1099 if e != nil { 1100 return c.InternalError(e, w, r) 1101 } 1102 ovList := analyticsVMapToOVList(vMap) 1103 1104 var vList [][]int64 1105 var legendList []string 1106 var i int 1107 for _, ovitem := range ovList { 1108 viewList := make([]int64, len(revLabelList)) 1109 for ii, val := range revLabelList { 1110 viewList[ii] = ovitem.viewMap[val] 1111 } 1112 vList = append(vList, viewList) 1113 lName, ok := p.GetOSPhrase(ovitem.name) 1114 if !ok { 1115 lName = ovitem.name 1116 } 1117 legendList = append(legendList, lName) 1118 if i >= 6 { 1119 break 1120 } 1121 i++ 1122 } 1123 graph := createTimeGraph(vList, labelList, legendList) 1124 1125 // TODO: Sort this slice 1126 systemItems, i := make([]c.PanelAnalyticsAgentsItem, len(osMap)), 0 1127 for system, count := range osMap { 1128 sSystem, ok := p.GetOSPhrase(system) 1129 if !ok { 1130 sSystem = system 1131 } 1132 systemItems[i] = c.PanelAnalyticsAgentsItem{ 1133 Agent: system, 1134 FriendlyAgent: sSystem, 1135 Count: count, 1136 } 1137 i++ 1138 } 1139 1140 pi := c.PanelAnalyticsDuoPage{bp, systemItems, graph, tr.Range} 1141 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_systems", pi}) 1142 } 1143 1144 func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 1145 bp, ferr := PreAnalyticsDetail(w, r, u) 1146 if ferr != nil { 1147 return ferr 1148 } 1149 bp.AddScript("chartist/chartist-plugin-legend.min.js") 1150 bp.AddSheet("chartist/chartist-plugin-legend.css") 1151 1152 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 1153 if e != nil { 1154 return c.LocalError(e.Error(), w, r, u) 1155 } 1156 revLabelList, labelList, viewMap := c.AnalyticsTimeRangeToLabelList(tr) 1157 1158 rows, e := qgen.NewAcc().Select("viewchunks_langs").Columns("count,lang,createdAt").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 1159 if e != nil && e != sql.ErrNoRows { 1160 return c.InternalError(e, w, r) 1161 } 1162 vMap, langMap, e := analyticsRowsToDuoMap(rows, labelList, viewMap) 1163 if e != nil { 1164 return c.InternalError(e, w, r) 1165 } 1166 ovList := analyticsVMapToOVList(vMap) 1167 1168 ex := strings.Split(r.FormValue("ex"), ",") 1169 inEx := func(name string) bool { 1170 for _, e := range ex { 1171 if e == name { 1172 return true 1173 } 1174 } 1175 return false 1176 } 1177 1178 var vList [][]int64 1179 var legendList []string 1180 var i int 1181 for _, ovitem := range ovList { 1182 if inEx(ovitem.name) { 1183 continue 1184 } 1185 lName, ok := p.GetHumanLangPhrase(ovitem.name) 1186 if !ok { 1187 lName = ovitem.name 1188 } 1189 if inEx(lName) { 1190 continue 1191 } 1192 1193 viewList := make([]int64, len(revLabelList)) 1194 for _, val := range revLabelList { 1195 viewList[i] = ovitem.viewMap[val] 1196 } 1197 vList = append(vList, viewList) 1198 legendList = append(legendList, lName) 1199 if i >= 6 { 1200 break 1201 } 1202 i++ 1203 } 1204 graph := createTimeGraph(vList, labelList, legendList) 1205 1206 // TODO: Can we de-duplicate these analytics functions further? 1207 // TODO: Sort this slice 1208 var langItems []c.PanelAnalyticsAgentsItem 1209 for lang, count := range langMap { 1210 if inEx(lang) { 1211 continue 1212 } 1213 lLang, ok := p.GetHumanLangPhrase(lang) 1214 if !ok { 1215 lLang = lang 1216 } 1217 if inEx(lLang) { 1218 continue 1219 } 1220 langItems = append(langItems, c.PanelAnalyticsAgentsItem{ 1221 Agent: lang, 1222 FriendlyAgent: lLang, 1223 Count: count, 1224 }) 1225 } 1226 1227 pi := c.PanelAnalyticsDuoPage{bp, langItems, graph, tr.Range} 1228 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_langs", pi}) 1229 } 1230 1231 func AnalyticsReferrers(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 1232 bp, ferr := buildBasePage(w, r, u, "analytics", "analytics") 1233 if ferr != nil { 1234 return ferr 1235 } 1236 tr, e := analyticsTimeRange(r.FormValue("timeRange")) 1237 if e != nil { 1238 return c.LocalError(e.Error(), w, r, u) 1239 } 1240 1241 rows, e := qgen.NewAcc().Select("viewchunks_referrers").Columns("count,domain").DateCutoff("createdAt", tr.Quantity, tr.Unit).Query() 1242 if e != nil && e != sql.ErrNoRows { 1243 return c.InternalError(e, w, r) 1244 } 1245 refMap, e := analyticsRowsToRefMap(rows) 1246 if e != nil { 1247 return c.InternalError(e, w, r) 1248 } 1249 showSpam := r.FormValue("spam") == "1" 1250 1251 isSpammy := func(domain string) bool { 1252 for _, substr := range c.SpammyDomainBits { 1253 if strings.Contains(domain, substr) { 1254 return true 1255 } 1256 } 1257 return false 1258 } 1259 1260 // TODO: Sort this slice 1261 var refItems []c.PanelAnalyticsAgentsItem 1262 for domain, count := range refMap { 1263 sdomain := c.SanitiseSingleLine(domain) 1264 if !showSpam && isSpammy(sdomain) { 1265 continue 1266 } 1267 refItems = append(refItems, c.PanelAnalyticsAgentsItem{ 1268 Agent: sdomain, 1269 Count: count, 1270 }) 1271 } 1272 1273 pi := c.PanelAnalyticsReferrersPage{bp, refItems, tr.Range, showSpam} 1274 return renderTemplate("panel", w, r, bp.Header, c.Panel{bp, "panel_analytics_right", "analytics", "panel_analytics_referrers", pi}) 1275 }