diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e38f0c..94f60cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * added storing hostname * added reading and filtering for hostnames * added reading visitors by weekday and hour +* added sorting by entry and exit rate +* improved entry and exit rate calculation * fixed storing milliseconds for timestamps * updated dependencies diff --git a/pkg/analyzer/filter_fields.go b/pkg/analyzer/filter_fields.go index 4de2232..58d991f 100644 --- a/pkg/analyzer/filter_fields.go +++ b/pkg/analyzer/filter_fields.go @@ -97,6 +97,16 @@ var ( Name: "entries", } + // FieldEntryRate is a query result column. + FieldEntryRate = Field{ + querySessions: `toFloat64OrDefault(entries / greatest((SELECT uniq(visitor_id, session_id)%s FROM "session"%s WHERE %s), 1))`, + queryPageViews: `toFloat64OrDefault(entries / greatest((SELECT uniq(visitor_id, session_id)%s FROM "session"%s WHERE %s), 1))`, + queryImported: `toFloat64OrDefault(entries / greatest((SELECT uniq(visitor_id, session_id)%s FROM "session"%s WHERE %s) + (SELECT sum(sessions) FROM "%s" WHERE %s), 1))`, + queryDirection: "DESC", + filterTime: true, + Name: "entry_rate", + } + // FieldExitPath is a query result column. FieldExitPath = Field{ querySessions: "exit_path", @@ -116,6 +126,16 @@ var ( Name: "exits", } + // FieldExitRate is a query result column. + FieldExitRate = Field{ + querySessions: `toFloat64OrDefault(exits / greatest((SELECT uniq(visitor_id, session_id)%s FROM "session"%s WHERE %s), 1))`, + queryPageViews: `toFloat64OrDefault(exits / greatest((SELECT uniq(visitor_id, session_id)%s FROM "session"%s WHERE %s), 1))`, + queryImported: `toFloat64OrDefault(exits / greatest((SELECT uniq(visitor_id, session_id)%s FROM "session"%s WHERE %s) + (SELECT sum(sessions) FROM "%s" WHERE %s), 1))`, + queryDirection: "DESC", + filterTime: true, + Name: "exit_rate", + } + // FieldVisitors is a query result column. FieldVisitors = Field{ querySessions: "uniq(t.visitor_id)", diff --git a/pkg/analyzer/pages.go b/pkg/analyzer/pages.go index 4dc8e0f..9ea47b7 100644 --- a/pkg/analyzer/pages.go +++ b/pkg/analyzer/pages.go @@ -145,6 +145,7 @@ func (pages *Pages) Entry(filter *Filter) ([]model.EntryStats, error) { FieldHostname, FieldEntryPath, FieldEntries, + FieldEntryRate, } groupBy := []Field{ FieldHostname, @@ -173,13 +174,6 @@ func (pages *Pages) Entry(filter *Filter) ([]model.EntryStats, error) { } pathList := getPathList(stats) - totalSessions, err := pages.totalSessions(filter) - - if err != nil { - return nil, err - } - - totalSessionsFloat64 := float64(totalSessions) total, err := pages.totalVisitorsSessions(filter, pathList) if err != nil { @@ -191,7 +185,6 @@ func (pages *Pages) Entry(filter *Filter) ([]model.EntryStats, error) { if stats[i].Path == total[j].Path { stats[i].Visitors = total[j].Visitors stats[i].Sessions = total[j].Sessions - stats[i].EntryRate = float64(stats[i].Entries) / totalSessionsFloat64 break } } @@ -243,6 +236,7 @@ func (pages *Pages) Exit(filter *Filter) ([]model.ExitStats, error) { FieldHostname, FieldExitPath, FieldExits, + FieldExitRate, } groupBy := []Field{ FieldHostname, @@ -270,13 +264,6 @@ func (pages *Pages) Exit(filter *Filter) ([]model.ExitStats, error) { return nil, err } - totalSessions, err := pages.totalSessions(filter) - - if err != nil { - return nil, err - } - - totalSessionsFloat64 := float64(totalSessions) pathList := getPathList(stats) total, err := pages.totalVisitorsSessions(filter, pathList) @@ -289,13 +276,6 @@ func (pages *Pages) Exit(filter *Filter) ([]model.ExitStats, error) { if stats[i].Path == total[j].Path { stats[i].Visitors = total[j].Visitors stats[i].Sessions = total[j].Sessions - - if totalSessions == 0 { - stats[i].ExitRate = 1 - } else { - stats[i].ExitRate = float64(stats[i].Exits) / totalSessionsFloat64 - } - break } } @@ -341,19 +321,6 @@ func (pages *Pages) Conversions(filter *Filter) (*model.ConversionsStats, error) return stats, nil } -func (pages *Pages) totalSessions(filter *Filter) (int, error) { - filter = pages.analyzer.getFilter(filter) - filterQuery, filterArgs := filter.buildTimeQuery() - query := fmt.Sprintf("SELECT uniq(visitor_id, session_id) FROM session %s HAVING sum(sign) > 0", filterQuery) - stats, err := pages.store.SelectTotalSessions(filter.Ctx, query, filterArgs...) - - if err != nil { - return 0, err - } - - return stats, nil -} - func (pages *Pages) totalVisitorsSessions(filter *Filter, paths []string) ([]model.TotalVisitorSessionStats, error) { if len(paths) == 0 { return []model.TotalVisitorSessionStats{}, nil diff --git a/pkg/analyzer/pages_test.go b/pkg/analyzer/pages_test.go index bc7392d..da2729c 100644 --- a/pkg/analyzer/pages_test.go +++ b/pkg/analyzer/pages_test.go @@ -1061,8 +1061,8 @@ func TestAnalyzer_EntryExitPages(t *testing.T) { assert.Equal(t, "/bar", entries[1].Path) assert.Equal(t, 8, entries[0].Entries) assert.Equal(t, 4, entries[1].Entries) - assert.InDelta(t, 0.8888, entries[0].EntryRate, 0.001) - assert.InDelta(t, 0.4444, entries[1].EntryRate, 0.001) + assert.InDelta(t, 0.6153, entries[0].EntryRate, 0.001) + assert.InDelta(t, 0.3076, entries[1].EntryRate, 0.001) exits, err = analyzer.Pages.Exit(&Filter{ From: util.PastDay(3), To: util.Today(), @@ -1076,9 +1076,9 @@ func TestAnalyzer_EntryExitPages(t *testing.T) { assert.Equal(t, 6, exits[0].Exits) assert.Equal(t, 5, exits[1].Exits) assert.Equal(t, 1, exits[2].Exits) - assert.InDelta(t, 0.6666, exits[0].ExitRate, 0.001) - assert.InDelta(t, 0.5555, exits[1].ExitRate, 0.001) - assert.InDelta(t, 0.1111, exits[2].ExitRate, 0.001) + assert.InDelta(t, 0.4615, exits[0].ExitRate, 0.001) + assert.InDelta(t, 0.3846, exits[1].ExitRate, 0.001) + assert.InDelta(t, 0.0769, exits[2].ExitRate, 0.001) entries, err = analyzer.Pages.Entry(&Filter{ From: util.PastDay(3), To: util.Today(), @@ -1092,7 +1092,7 @@ func TestAnalyzer_EntryExitPages(t *testing.T) { assert.Equal(t, "/", entries[0].Path) assert.Equal(t, "Home", entries[0].Title) assert.Equal(t, 8, entries[0].Entries) - assert.InDelta(t, 0.8888, entries[0].EntryRate, 0.001) + assert.InDelta(t, 0.6153, entries[0].EntryRate, 0.001) assert.Equal(t, 23, entries[0].AverageTimeSpentSeconds) exits, err = analyzer.Pages.Exit(&Filter{ From: util.PastDay(3), @@ -1106,7 +1106,7 @@ func TestAnalyzer_EntryExitPages(t *testing.T) { assert.Equal(t, "/bar", exits[0].Path) assert.Equal(t, "Bar", exits[0].Title) assert.Equal(t, 6, exits[0].Exits) - assert.InDelta(t, 0.6666, exits[0].ExitRate, 0.001) + assert.InDelta(t, 0.4615, exits[0].ExitRate, 0.001) } func TestAnalyzer_EntryExitPagesSortVisitors(t *testing.T) { @@ -1138,6 +1138,20 @@ func TestAnalyzer_EntryExitPagesSortVisitors(t *testing.T) { assert.Equal(t, "/", entries[1].Path) assert.Equal(t, 1, entries[0].Visitors) assert.Equal(t, 2, entries[1].Visitors) + entries, err = analyzer.Pages.Entry(&Filter{Sort: []Sort{{Field: FieldEntryRate, Direction: pkg.DirectionDESC}}}) + assert.NoError(t, err) + assert.Len(t, entries, 2) + assert.Equal(t, "/", entries[0].Path) + assert.Equal(t, "/foo", entries[1].Path) + assert.Equal(t, 2, entries[0].Visitors) + assert.Equal(t, 1, entries[1].Visitors) + entries, err = analyzer.Pages.Entry(&Filter{Sort: []Sort{{Field: FieldEntryRate, Direction: pkg.DirectionASC}}}) + assert.NoError(t, err) + assert.Len(t, entries, 2) + assert.Equal(t, "/foo", entries[0].Path) + assert.Equal(t, "/", entries[1].Path) + assert.Equal(t, 1, entries[0].Visitors) + assert.Equal(t, 2, entries[1].Visitors) exits, err := analyzer.Pages.Exit(&Filter{Sort: []Sort{{Field: FieldVisitors, Direction: pkg.DirectionDESC}}}) assert.NoError(t, err) assert.Len(t, exits, 2) @@ -1152,6 +1166,20 @@ func TestAnalyzer_EntryExitPagesSortVisitors(t *testing.T) { assert.Equal(t, "/", exits[1].Path) assert.Equal(t, 1, exits[0].Visitors) assert.Equal(t, 2, exits[1].Visitors) + exits, err = analyzer.Pages.Exit(&Filter{Sort: []Sort{{Field: FieldExitRate, Direction: pkg.DirectionDESC}}}) + assert.NoError(t, err) + assert.Len(t, exits, 2) + assert.Equal(t, "/", exits[0].Path) + assert.Equal(t, "/foo", exits[1].Path) + assert.Equal(t, 2, exits[0].Visitors) + assert.Equal(t, 1, exits[1].Visitors) + exits, err = analyzer.Pages.Exit(&Filter{Sort: []Sort{{Field: FieldExitRate, Direction: pkg.DirectionASC}}}) + assert.NoError(t, err) + assert.Len(t, exits, 2) + assert.Equal(t, "/foo", exits[0].Path) + assert.Equal(t, "/", exits[1].Path) + assert.Equal(t, 1, exits[0].Visitors) + assert.Equal(t, 2, exits[1].Visitors) } func TestAnalyzer_EntryExitPagesEvents(t *testing.T) { diff --git a/pkg/db/client.go b/pkg/db/client.go index 2f26a21..e40450f 100644 --- a/pkg/db/client.go +++ b/pkg/db/client.go @@ -1269,6 +1269,7 @@ func (client *Client) SelectEntryStats(ctx context.Context, includeTitle bool, q if err := rows.Scan(&result.Hostname, &result.Path, &result.Entries, + &result.EntryRate, &result.Title); err != nil { return nil, err } @@ -1281,7 +1282,8 @@ func (client *Client) SelectEntryStats(ctx context.Context, includeTitle bool, q if err := rows.Scan(&result.Hostname, &result.Path, - &result.Entries); err != nil { + &result.Entries, + &result.EntryRate); err != nil { return nil, err } @@ -1310,6 +1312,7 @@ func (client *Client) SelectExitStats(ctx context.Context, includeTitle bool, qu if err := rows.Scan(&result.Hostname, &result.Path, &result.Exits, + &result.ExitRate, &result.Title); err != nil { return nil, err } @@ -1322,7 +1325,8 @@ func (client *Client) SelectExitStats(ctx context.Context, includeTitle bool, qu if err := rows.Scan(&result.Hostname, &result.Path, - &result.Exits); err != nil { + &result.Exits, + &result.ExitRate); err != nil { return nil, err }