Antonio Mika
·
2026-10-25
1package postgres
2
3import (
4 "fmt"
5 "sort"
6 "sync"
7 "time"
8
9 "github.com/picosh/pico/pkg/db"
10)
11
12func (me *PsqlDB) InsertVisit(visit *db.AnalyticsVisits) error {
13 _, err := me.Db.Exec(
14 `INSERT INTO analytics_visits (user_id, project_id, post_id, namespace, host, path, ip_address, user_agent, referer, status, content_type, is_mobile) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);`,
15 visit.UserID,
16 newNullString(visit.ProjectID),
17 newNullString(visit.PostID),
18 newNullString(visit.Namespace),
19 visit.Host,
20 visit.Path,
21 visit.IpAddress,
22 visit.UserAgent,
23 visit.Referer,
24 visit.Status,
25 visit.ContentType,
26 db.IsMobileUserAgent(visit.UserAgent),
27 )
28 return err
29}
30
31func visitFilterBy(opts *db.SummaryOpts) (string, string) {
32 where := ""
33 val := ""
34 if opts.Host != "" {
35 where = "host"
36 val = opts.Host
37 } else if opts.Path != "" {
38 where = "path"
39 val = opts.Path
40 }
41
42 return where, val
43}
44
45func (me *PsqlDB) visitUnique(opts *db.SummaryOpts) ([]*db.VisitInterval, error) {
46 var intervals, currentIntervals []*db.VisitInterval
47 var sumErr, rawErr error
48
49 var wg sync.WaitGroup
50 wg.Add(2)
51
52 go func() {
53 defer wg.Done()
54 intervals, sumErr = me.visitUniqueFromSummary(opts)
55 }()
56 go func() {
57 defer wg.Done()
58 currentIntervals, rawErr = me.visitUniqueFromRaw(opts)
59 }()
60
61 wg.Wait()
62
63 if sumErr != nil {
64 return nil, fmt.Errorf("query summary visits: %w", sumErr)
65 }
66 if rawErr != nil {
67 return nil, fmt.Errorf("query raw visits: %w", rawErr)
68 }
69
70 // Merge: current month data may overlap with summary data, combine counts
71 return mergeVisitIntervals(intervals, currentIntervals), nil
72}
73
74// visitUniqueFromSummary reads unique visitor counts from analytics_monthly_visits for historical data.
75func (me *PsqlDB) visitUniqueFromSummary(opts *db.SummaryOpts) ([]*db.VisitInterval, error) {
76 now := time.Now()
77 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
78 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
79
80 // If origin is in the previous month or later, raw data covers it — no summary to fetch.
81 if !opts.Origin.Before(previousMonthStart) {
82 return nil, nil
83 }
84
85 where := ""
86 args := []interface{}{opts.UserID, opts.Origin, currentMonthStart}
87 argIdx := 4
88 if opts.Host != "" {
89 where = "AND host = $" + fmt.Sprintf("%d", argIdx)
90 args = append(args, opts.Host)
91 }
92
93 query := fmt.Sprintf(`
94 SELECT
95 date_trunc('%s', visit_date)::timestamptz as interval_start,
96 sum(unique_visits) as unique_visitors,
97 sum(mobile_visits) as mobile_visits,
98 sum(desktop_visits) as desktop_visits
99 FROM analytics_monthly_visits
100 WHERE user_id = $1 AND visit_date >= $2 AND visit_date < $3 %s
101 GROUP BY interval_start
102 ORDER BY interval_start`, opts.Interval, where)
103
104 rows, err := me.Db.Queryx(query, args...)
105 if err != nil {
106 return nil, err
107 }
108 defer func() { _ = rows.Close() }()
109
110 var intervals []*db.VisitInterval
111 for rows.Next() {
112 interval := &db.VisitInterval{}
113 if err := rows.Scan(&interval.Interval, &interval.Visitors, &interval.MobileVisitors, &interval.DesktopVisitors); err != nil {
114 return nil, err
115 }
116 intervals = append(intervals, interval)
117 }
118 return intervals, rows.Err()
119}
120
121// visitUniqueFromRaw reads unique visitor counts from analytics_visits for the previous and current months.
122// This covers the gap between the last aggregated month and the current month.
123func (me *PsqlDB) visitUniqueFromRaw(opts *db.SummaryOpts) ([]*db.VisitInterval, error) {
124 now := time.Now()
125 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
126 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
127
128 where, with := visitFilterBy(opts)
129
130 // Determine the effective start: max(origin, previousMonthStart)
131 effectiveStart := previousMonthStart
132 if opts.Origin.After(previousMonthStart) {
133 effectiveStart = opts.Origin
134 }
135
136 uniqueVisitors := fmt.Sprintf(`
137 SELECT
138 date_trunc('%s', created_at)::timestamptz as interval_start,
139 count(DISTINCT CASE WHEN is_mobile THEN ip_address END) as mobile_visitors,
140 count(DISTINCT CASE WHEN NOT is_mobile THEN ip_address END) as desktop_visitors,
141 count(DISTINCT ip_address) as unique_visitors
142 FROM analytics_visits
143 WHERE created_at >= $1 AND created_at < $2 AND %s = $3 AND user_id = $4 AND status <> 404
144 GROUP BY interval_start
145 ORDER BY interval_start`,
146 opts.Interval, where)
147
148 rows, err := me.Db.Queryx(uniqueVisitors, effectiveStart, currentMonthStart.AddDate(0, 1, 0), with, opts.UserID)
149 if err != nil {
150 return nil, err
151 }
152 defer func() { _ = rows.Close() }()
153
154 var intervals []*db.VisitInterval
155 for rows.Next() {
156 interval := &db.VisitInterval{}
157 if err := rows.Scan(&interval.Interval, &interval.MobileVisitors, &interval.DesktopVisitors, &interval.Visitors); err != nil {
158 return nil, err
159 }
160 intervals = append(intervals, interval)
161 }
162 return intervals, rows.Err()
163}
164
165// mergeVisitIntervals combines historical (summary table) and current (raw) intervals.
166// Summary data is preferred when both sources have the same interval to avoid double-counting.
167func mergeVisitIntervals(historical, current []*db.VisitInterval) []*db.VisitInterval {
168 if len(historical) == 0 {
169 return current
170 }
171 if len(current) == 0 {
172 return historical
173 }
174
175 // Build a map by interval timestamp for merging.
176 // Summary data takes precedence over raw data for the same interval
177 // (e.g. when a month is aggregated but raw data still exists in a local dump).
178 intervalMap := make(map[int64]*db.VisitInterval)
179 for _, ci := range current {
180 ts := ci.Interval.Unix()
181 intervalMap[ts] = ci
182 }
183 for _, hi := range historical {
184 ts := hi.Interval.Unix()
185 intervalMap[ts] = hi // summary overwrites raw if both exist
186 }
187
188 // Sort by interval
189 result := make([]*db.VisitInterval, 0, len(intervalMap))
190 for _, iv := range intervalMap {
191 result = append(result, iv)
192 }
193 sort.Slice(result, func(i, j int) bool {
194 return result[i].Interval.Before(*result[j].Interval)
195 })
196 return result
197}
198
199// mergeTopUrls combines historical and current top URLs, summing counts for overlapping URLs,
200// then returns the top 10 by total count.
201func mergeTopUrls(historical, current []*db.VisitUrl) []*db.VisitUrl {
202 if len(historical) == 0 {
203 return current
204 }
205 if len(current) == 0 {
206 return historical
207 }
208
209 // Build a map by URL for merging
210 urlMap := make(map[string]*db.VisitUrl)
211 for _, hu := range historical {
212 urlMap[hu.Url] = hu
213 }
214
215 for _, cu := range current {
216 if existing, ok := urlMap[cu.Url]; ok {
217 existing.Count += cu.Count
218 } else {
219 urlMap[cu.Url] = cu
220 }
221 }
222
223 // Sort by count descending and take top 10
224 result := make([]*db.VisitUrl, 0, len(urlMap))
225 for _, u := range urlMap {
226 result = append(result, u)
227 }
228 sort.Slice(result, func(i, j int) bool {
229 return result[i].Count > result[j].Count
230 })
231 if len(result) > 10 {
232 result = result[:10]
233 }
234 return result
235}
236
237// mergeTopReferers combines historical and current top referers, summing counts for overlapping referers,
238// then returns the top 10 by total count.
239func mergeTopReferers(historical, current []*db.VisitUrl) []*db.VisitUrl {
240 return mergeTopUrls(historical, current) // Same logic as mergeTopUrls
241}
242
243// mergeHosts combines historical and current hosts, summing counts for overlapping hosts,
244// then returns sorted by total count descending.
245func mergeHosts(historical, current []*db.VisitUrl) []*db.VisitUrl {
246 if len(historical) == 0 {
247 return current
248 }
249 if len(current) == 0 {
250 return historical
251 }
252
253 // Build a map by host for merging
254 hostMap := make(map[string]*db.VisitUrl)
255 for _, hu := range historical {
256 hostMap[hu.Url] = hu
257 }
258
259 for _, cu := range current {
260 if existing, ok := hostMap[cu.Url]; ok {
261 existing.Count += cu.Count
262 } else {
263 hostMap[cu.Url] = cu
264 }
265 }
266
267 // Sort by count descending
268 result := make([]*db.VisitUrl, 0, len(hostMap))
269 for _, h := range hostMap {
270 result = append(result, h)
271 }
272 sort.Slice(result, func(i, j int) bool {
273 return result[i].Count > result[j].Count
274 })
275 return result
276}
277
278func (me *PsqlDB) visitReferer(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
279 var historical, current []*db.VisitUrl
280 var histErr, rawErr error
281
282 var wg sync.WaitGroup
283 wg.Add(2)
284
285 go func() {
286 defer wg.Done()
287 historical, histErr = me.visitRefererFromSummary(opts)
288 }()
289 go func() {
290 defer wg.Done()
291 current, rawErr = me.visitRefererFromRaw(opts)
292 }()
293
294 wg.Wait()
295
296 if histErr != nil {
297 return nil, fmt.Errorf("query summary referers: %w", histErr)
298 }
299 if rawErr != nil {
300 return nil, fmt.Errorf("query raw referers: %w", rawErr)
301 }
302
303 return mergeTopReferers(historical, current), nil
304}
305
306// visitRefererFromSummary reads top referers from analytics_monthly_top_referers for historical data.
307func (me *PsqlDB) visitRefererFromSummary(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
308 now := time.Now()
309 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
310 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
311
312 // If origin is in the previous month or later, raw data covers it — no summary to fetch.
313 if !opts.Origin.Before(previousMonthStart) {
314 return nil, nil
315 }
316
317 // Clamp origin to month boundary for summary table lookup
318 originMonthStart := time.Date(opts.Origin.Year(), opts.Origin.Month(), 1, 0, 0, 0, 0, time.UTC)
319
320 where := ""
321 args := []interface{}{opts.UserID, originMonthStart, previousMonthStart}
322 if opts.Host != "" {
323 where = "AND host = $4"
324 args = append(args, opts.Host)
325 }
326
327 query := fmt.Sprintf(`
328 SELECT referer, sum(unique_visits) as total_visits
329 FROM analytics_monthly_top_referers
330 WHERE user_id = $1 AND month >= $2 AND month < $3 %s
331 GROUP BY referer
332 ORDER BY total_visits DESC
333 LIMIT 10`, where)
334
335 rows, err := me.Db.Queryx(query, args...)
336 if err != nil {
337 return nil, err
338 }
339 defer func() { _ = rows.Close() }()
340
341 var results []*db.VisitUrl
342 for rows.Next() {
343 result := &db.VisitUrl{}
344 if err := rows.Scan(&result.Url, &result.Count); err != nil {
345 return nil, err
346 }
347 results = append(results, result)
348 }
349 return results, rows.Err()
350}
351
352// visitRefererFromRaw reads top referers from analytics_visits for the previous and current months.
353func (me *PsqlDB) visitRefererFromRaw(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
354 now := time.Now()
355 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
356 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
357
358 where, with := visitFilterBy(opts)
359
360 // Determine the effective start: max(origin, previousMonthStart)
361 effectiveStart := previousMonthStart
362 if opts.Origin.After(previousMonthStart) {
363 effectiveStart = opts.Origin
364 }
365
366 topUrls := fmt.Sprintf(`
367 SELECT
368 referer,
369 count(DISTINCT ip_address) as referer_count
370 FROM analytics_visits
371 WHERE created_at >= $1 AND created_at < $2 AND %s = $3 AND user_id = $4 AND referer <> '' AND status <> 404
372 GROUP BY referer
373 ORDER BY referer_count DESC
374 LIMIT 10`, where)
375
376 rows, err := me.Db.Queryx(topUrls, effectiveStart, currentMonthStart.AddDate(0, 1, 0), with, opts.UserID)
377 if err != nil {
378 return nil, err
379 }
380 defer func() { _ = rows.Close() }()
381
382 var results []*db.VisitUrl
383 for rows.Next() {
384 result := &db.VisitUrl{}
385 if err := rows.Scan(&result.Url, &result.Count); err != nil {
386 return nil, err
387 }
388 results = append(results, result)
389 }
390 return results, rows.Err()
391}
392
393func (me *PsqlDB) visitUrl(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
394 var historical, current []*db.VisitUrl
395 var histErr, rawErr error
396
397 var wg sync.WaitGroup
398 wg.Add(2)
399
400 go func() {
401 defer wg.Done()
402 historical, histErr = me.visitUrlFromSummary(opts)
403 }()
404 go func() {
405 defer wg.Done()
406 current, rawErr = me.visitUrlFromRaw(opts)
407 }()
408
409 wg.Wait()
410
411 if histErr != nil {
412 return nil, fmt.Errorf("query summary urls: %w", histErr)
413 }
414 if rawErr != nil {
415 return nil, fmt.Errorf("query raw urls: %w", rawErr)
416 }
417
418 return mergeTopUrls(historical, current), nil
419}
420
421// visitUrlFromSummary reads top URLs from analytics_monthly_top_urls for historical data.
422func (me *PsqlDB) visitUrlFromSummary(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
423 now := time.Now()
424 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
425 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
426
427 // If origin is in the previous month or later, raw data covers it — no summary to fetch.
428 if !opts.Origin.Before(previousMonthStart) {
429 return nil, nil
430 }
431
432 // Clamp origin to month boundary for summary table lookup
433 originMonthStart := time.Date(opts.Origin.Year(), opts.Origin.Month(), 1, 0, 0, 0, 0, time.UTC)
434
435 where := ""
436 args := []interface{}{opts.UserID, originMonthStart, previousMonthStart}
437 if opts.Host != "" {
438 where = "AND host = $4"
439 args = append(args, opts.Host)
440 }
441
442 query := fmt.Sprintf(`
443 SELECT path, sum(unique_visits) as total_visits
444 FROM analytics_monthly_top_urls
445 WHERE user_id = $1 AND month >= $2 AND month < $3 AND status_code <> 404 %s
446 GROUP BY path
447 ORDER BY total_visits DESC
448 LIMIT 10`, where)
449
450 rows, err := me.Db.Queryx(query, args...)
451 if err != nil {
452 return nil, err
453 }
454 defer func() { _ = rows.Close() }()
455
456 var results []*db.VisitUrl
457 for rows.Next() {
458 result := &db.VisitUrl{}
459 if err := rows.Scan(&result.Url, &result.Count); err != nil {
460 return nil, err
461 }
462 results = append(results, result)
463 }
464 return results, rows.Err()
465}
466
467// visitUrlFromRaw reads top URLs from analytics_visits for the previous and current months.
468func (me *PsqlDB) visitUrlFromRaw(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
469 now := time.Now()
470 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
471 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
472
473 where, with := visitFilterBy(opts)
474
475 // Determine the effective start: max(origin, previousMonthStart)
476 effectiveStart := previousMonthStart
477 if opts.Origin.After(previousMonthStart) {
478 effectiveStart = opts.Origin
479 }
480
481 topUrls := fmt.Sprintf(`
482 SELECT
483 path,
484 count(DISTINCT ip_address) as path_count
485 FROM analytics_visits
486 WHERE created_at >= $1 AND created_at < $2 AND %s = $3 AND user_id = $4 AND path <> '' AND status <> 404
487 GROUP BY path
488 ORDER BY path_count DESC
489 LIMIT 10`, where)
490
491 rows, err := me.Db.Queryx(topUrls, effectiveStart, currentMonthStart.AddDate(0, 1, 0), with, opts.UserID)
492 if err != nil {
493 return nil, err
494 }
495 defer func() { _ = rows.Close() }()
496
497 var results []*db.VisitUrl
498 for rows.Next() {
499 result := &db.VisitUrl{}
500 if err := rows.Scan(&result.Url, &result.Count); err != nil {
501 return nil, err
502 }
503 results = append(results, result)
504 }
505 return results, rows.Err()
506}
507
508func (me *PsqlDB) VisitUrlNotFound(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
509 limit := opts.Limit
510 if limit == 0 {
511 limit = 10
512 }
513
514 var historical, current []*db.VisitUrl
515 var histErr, rawErr error
516
517 var wg sync.WaitGroup
518 wg.Add(2)
519
520 go func() {
521 defer wg.Done()
522 historical, histErr = me.visitUrlNotFoundFromSummary(opts, limit)
523 }()
524 go func() {
525 defer wg.Done()
526 current, rawErr = me.visitUrlNotFoundFromRaw(opts, limit)
527 }()
528
529 wg.Wait()
530
531 if histErr != nil {
532 return nil, fmt.Errorf("query summary 404 urls: %w", histErr)
533 }
534 if rawErr != nil {
535 return nil, fmt.Errorf("query raw 404 urls: %w", rawErr)
536 }
537
538 return mergeTopUrls(historical, current), nil
539}
540
541// visitUrlNotFoundFromSummary reads top 404 URLs from analytics_monthly_top_urls for historical data.
542func (me *PsqlDB) visitUrlNotFoundFromSummary(opts *db.SummaryOpts, limit int) ([]*db.VisitUrl, error) {
543 now := time.Now()
544 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
545 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
546
547 // If origin is in the previous month or later, raw data covers it — no summary to fetch.
548 if !opts.Origin.Before(previousMonthStart) {
549 return nil, nil
550 }
551
552 // Clamp origin to month boundary for summary table lookup
553 originMonthStart := time.Date(opts.Origin.Year(), opts.Origin.Month(), 1, 0, 0, 0, 0, time.UTC)
554
555 where := ""
556 args := []interface{}{opts.UserID, originMonthStart, previousMonthStart}
557 argIdx := 4
558 if opts.Host != "" {
559 where = "AND host = $" + fmt.Sprintf("%d", argIdx)
560 args = append(args, opts.Host)
561 }
562
563 query := fmt.Sprintf(`
564 SELECT path, sum(unique_visits) as total_visits
565 FROM analytics_monthly_top_urls
566 WHERE user_id = $1 AND month >= $2 AND month < $3 AND status_code = 404 %s
567 GROUP BY path
568 ORDER BY total_visits DESC
569 LIMIT %d`, where, limit)
570
571 rows, err := me.Db.Queryx(query, args...)
572 if err != nil {
573 return nil, err
574 }
575 defer func() { _ = rows.Close() }()
576
577 var results []*db.VisitUrl
578 for rows.Next() {
579 result := &db.VisitUrl{}
580 if err := rows.Scan(&result.Url, &result.Count); err != nil {
581 return nil, err
582 }
583 results = append(results, result)
584 }
585 return results, rows.Err()
586}
587
588// visitUrlNotFoundFromRaw reads top 404 URLs from analytics_visits for the previous and current months.
589func (me *PsqlDB) visitUrlNotFoundFromRaw(opts *db.SummaryOpts, limit int) ([]*db.VisitUrl, error) {
590 now := time.Now()
591 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
592 previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
593
594 where, with := visitFilterBy(opts)
595
596 // Determine the effective start: max(origin, previousMonthStart)
597 effectiveStart := previousMonthStart
598 if opts.Origin.After(previousMonthStart) {
599 effectiveStart = opts.Origin
600 }
601
602 topUrls := fmt.Sprintf(`
603 SELECT
604 path,
605 count(DISTINCT ip_address) as path_count
606 FROM analytics_visits
607 WHERE created_at >= $1 AND created_at < $2 AND %s = $3 AND user_id = $4 AND path <> '' AND status = 404
608 GROUP BY path
609 ORDER BY path_count DESC
610 LIMIT %d`, where, limit)
611
612 rows, err := me.Db.Queryx(topUrls, effectiveStart, currentMonthStart.AddDate(0, 1, 0), with, opts.UserID)
613 if err != nil {
614 return nil, err
615 }
616 defer func() { _ = rows.Close() }()
617
618 var results []*db.VisitUrl
619 for rows.Next() {
620 result := &db.VisitUrl{}
621 if err := rows.Scan(&result.Url, &result.Count); err != nil {
622 return nil, err
623 }
624 results = append(results, result)
625 }
626 return results, rows.Err()
627}
628
629func (me *PsqlDB) visitHost(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
630 var historical, current []*db.VisitUrl
631 var histErr, rawErr error
632
633 var wg sync.WaitGroup
634 wg.Add(2)
635
636 go func() {
637 defer wg.Done()
638 historical, histErr = me.visitHostFromSummary(opts)
639 }()
640 go func() {
641 defer wg.Done()
642 current, rawErr = me.visitHostFromRaw(opts)
643 }()
644
645 wg.Wait()
646
647 if histErr != nil {
648 return nil, fmt.Errorf("query summary hosts: %w", histErr)
649 }
650 if rawErr != nil {
651 return nil, fmt.Errorf("query raw hosts: %w", rawErr)
652 }
653
654 return mergeHosts(historical, current), nil
655}
656
657// visitHostFromSummary reads host data from analytics_user_sites for historical data.
658func (me *PsqlDB) visitHostFromSummary(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
659 rows, err := me.Db.Queryx(`
660 SELECT host, total_visits
661 FROM analytics_user_sites
662 WHERE user_id = $1 AND host <> ''
663 ORDER BY total_visits DESC`, opts.UserID)
664 if err != nil {
665 return nil, err
666 }
667 defer func() { _ = rows.Close() }()
668
669 var results []*db.VisitUrl
670 for rows.Next() {
671 result := &db.VisitUrl{}
672 if err := rows.Scan(&result.Url, &result.Count); err != nil {
673 return nil, err
674 }
675 results = append(results, result)
676 }
677 return results, rows.Err()
678}
679
680// visitHostFromRaw reads hosts from analytics_visits for the current month that aren't in summary.
681func (me *PsqlDB) visitHostFromRaw(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
682 now := time.Now()
683 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
684
685 rows, err := me.Db.Queryx(`
686 SELECT host, count(DISTINCT ip_address) as host_count
687 FROM analytics_visits
688 WHERE created_at >= $1 AND user_id = $2 AND host <> ''
689 GROUP BY host
690 ORDER BY host_count DESC`, currentMonthStart, opts.UserID)
691 if err != nil {
692 return nil, err
693 }
694 defer func() { _ = rows.Close() }()
695
696 var results []*db.VisitUrl
697 for rows.Next() {
698 result := &db.VisitUrl{}
699 if err := rows.Scan(&result.Url, &result.Count); err != nil {
700 return nil, err
701 }
702 results = append(results, result)
703 }
704 return results, rows.Err()
705}
706
707func (me *PsqlDB) VisitSummary(opts *db.SummaryOpts) (*db.SummaryVisits, error) {
708 var (
709 visitors []*db.VisitInterval
710 urls []*db.VisitUrl
711 refs []*db.VisitUrl
712 notFound []*db.VisitUrl
713 visitorsErr error
714 urlsErr error
715 refsErr error
716 nfErr error
717 )
718
719 var wg sync.WaitGroup
720 wg.Add(4)
721
722 go func() {
723 defer wg.Done()
724 visitors, visitorsErr = me.visitUnique(opts)
725 }()
726 go func() {
727 defer wg.Done()
728 urls, urlsErr = me.visitUrl(opts)
729 }()
730 go func() {
731 defer wg.Done()
732 refs, refsErr = me.visitReferer(opts)
733 }()
734 go func() {
735 defer wg.Done()
736 notFound, nfErr = me.VisitUrlNotFound(opts)
737 }()
738
739 wg.Wait()
740
741 // Return the first error encountered
742 for _, err := range []error{visitorsErr, urlsErr, refsErr, nfErr} {
743 if err != nil {
744 return nil, err
745 }
746 }
747
748 return &db.SummaryVisits{
749 Intervals: visitors,
750 TopUrls: urls,
751 TopReferers: refs,
752 NotFoundUrls: notFound,
753 }, nil
754}
755
756func (me *PsqlDB) FindVisitSiteList(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
757 return me.visitHost(opts)
758}