2024-11-12 22:48:21 +00:00
|
|
|
package analyticsApi
|
|
|
|
|
|
|
|
import (
|
2024-11-13 01:51:11 +00:00
|
|
|
"encoding/json"
|
2024-11-12 22:48:21 +00:00
|
|
|
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/ldapApi"
|
|
|
|
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/logger"
|
|
|
|
"github.com/labstack/echo/v5"
|
|
|
|
"github.com/pocketbase/dbx"
|
|
|
|
"github.com/pocketbase/pocketbase"
|
|
|
|
"github.com/pocketbase/pocketbase/apis"
|
|
|
|
"github.com/pocketbase/pocketbase/core"
|
2024-11-12 23:35:28 +00:00
|
|
|
"strconv"
|
2024-11-12 22:48:21 +00:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2024-11-12 23:31:24 +00:00
|
|
|
// initPageViewCount
|
2024-11-12 22:48:21 +00:00
|
|
|
//
|
2024-11-13 00:05:14 +00:00
|
|
|
// This endpoint is used to retrieve paginated page view counts for each unique path.
|
2024-11-12 23:31:24 +00:00
|
|
|
// It adds the following endpoint to the app:
|
|
|
|
// GET /api/analytics/pageViewCount
|
|
|
|
//
|
2024-11-13 00:05:14 +00:00
|
|
|
// Query parameters:
|
2024-11-12 23:31:24 +00:00
|
|
|
//
|
2024-11-13 00:05:14 +00:00
|
|
|
// ?startDate=ISO 8601 formatted date (optional)
|
|
|
|
// ?page=<page number>
|
|
|
|
// ?perPage=<results per page>
|
|
|
|
// ?sort=<ASC|DESC> (default is ASC)
|
2024-11-12 23:31:24 +00:00
|
|
|
//
|
2024-11-13 00:05:14 +00:00
|
|
|
// - `startDate`: Optional start date in ISO 8601 format (e.g., "2024-11-12T15:04:05Z").
|
|
|
|
// - `page`: Page number for pagination (default is 1).
|
|
|
|
// - `perPage`: Number of results per page (default is 10).
|
|
|
|
// - `sort`: Sorting order, either `ASC` or `DESC` (default is ASC).
|
2024-11-12 23:31:24 +00:00
|
|
|
//
|
|
|
|
// Response format:
|
|
|
|
//
|
|
|
|
// {
|
2024-11-13 00:05:14 +00:00
|
|
|
// "page": <current page number>,
|
|
|
|
// "perPage": <results per page>,
|
|
|
|
// "totalItems": <total number of items matching the filter>,
|
|
|
|
// "totalPages": <total number of pages>,
|
|
|
|
// "items": [
|
2024-11-12 23:31:24 +00:00
|
|
|
// {
|
2024-11-13 00:05:14 +00:00
|
|
|
// "id": <unique row id>,
|
|
|
|
// "path": "<path>",
|
|
|
|
// "count": <total view count since startDate>,
|
|
|
|
// "last_30_days_data": [
|
|
|
|
// {
|
|
|
|
// "date": "YYYY-MM-DDTHH:MM:SSZ",
|
|
|
|
// "count": <daily count>
|
|
|
|
// },
|
|
|
|
// ...
|
|
|
|
// ]
|
2024-11-12 23:31:24 +00:00
|
|
|
// },
|
|
|
|
// ...
|
|
|
|
// ]
|
2024-11-13 00:05:14 +00:00
|
|
|
// }
|
2024-11-12 23:31:24 +00:00
|
|
|
func initPageViewCount(app *pocketbase.PocketBase, e *core.ServeEvent) {
|
|
|
|
|
|
|
|
e.Router.GET("/api/analytics/pageViewCount", func(c echo.Context) error {
|
|
|
|
|
|
|
|
// Check if user is in admin group
|
|
|
|
if err := ldapApi.UserIsInAdminGroup(app, c); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get startDate from query parameter
|
|
|
|
startDateParam := c.QueryParam("startDate")
|
|
|
|
var startDate time.Time
|
|
|
|
var err error
|
|
|
|
if startDateParam != "" {
|
|
|
|
startDate, err = time.Parse(time.RFC3339, startDateParam)
|
|
|
|
if err != nil {
|
|
|
|
return apis.NewBadRequestError("Invalid start date format, expected ISO 8601", err)
|
|
|
|
}
|
2024-11-13 01:32:45 +00:00
|
|
|
} else {
|
|
|
|
// Default to the beginning of time if not provided
|
|
|
|
startDate = time.Time{}
|
2024-11-12 23:31:24 +00:00
|
|
|
}
|
|
|
|
|
2024-11-13 00:05:14 +00:00
|
|
|
// Pagination and sorting parameters
|
2024-11-12 23:35:28 +00:00
|
|
|
page, _ := strconv.Atoi(c.QueryParam("page"))
|
|
|
|
if page < 1 {
|
|
|
|
page = 1
|
|
|
|
}
|
|
|
|
|
|
|
|
perPage, _ := strconv.Atoi(c.QueryParam("perPage"))
|
|
|
|
if perPage < 1 {
|
|
|
|
perPage = 10
|
|
|
|
}
|
|
|
|
|
|
|
|
sort := c.QueryParam("sort")
|
2024-11-13 01:32:45 +00:00
|
|
|
if sort != "ASC" && sort != "DESC" {
|
|
|
|
sort = "DESC" // default sorting order
|
2024-11-12 23:35:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
offset := (page - 1) * perPage
|
|
|
|
|
2024-11-13 00:05:14 +00:00
|
|
|
// Fetch total items for pagination
|
|
|
|
var totalItems int
|
|
|
|
err = app.Dao().DB().
|
|
|
|
NewQuery(`
|
|
|
|
SELECT COUNT(DISTINCT path) as total
|
|
|
|
FROM analyticsPageViews
|
|
|
|
WHERE created >= {:startDate}
|
|
|
|
`).
|
|
|
|
Bind(dbx.Params{
|
2024-11-13 01:32:45 +00:00
|
|
|
"startDate": startDate.Format("2006-01-02 15:04:05"),
|
2024-11-13 00:05:14 +00:00
|
|
|
}).
|
|
|
|
Row(&totalItems)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return apis.NewApiError(500, "Failed to count total items", err)
|
2024-11-12 23:31:24 +00:00
|
|
|
}
|
|
|
|
|
2024-11-13 00:05:14 +00:00
|
|
|
totalPages := (totalItems + perPage - 1) / perPage // Calculate total pages
|
|
|
|
|
|
|
|
// Define the structure for items
|
|
|
|
type Last30DaysData struct {
|
|
|
|
Date string `json:"date"`
|
|
|
|
Count int `json:"count"`
|
|
|
|
}
|
2024-11-12 23:31:24 +00:00
|
|
|
|
2024-11-13 01:51:11 +00:00
|
|
|
type Item struct {
|
|
|
|
Path string `json:"path"`
|
|
|
|
Count int `json:"count"`
|
|
|
|
Last30DaysData []Last30DaysData `json:"last_30_days_data"`
|
|
|
|
}
|
|
|
|
|
2024-11-13 01:32:45 +00:00
|
|
|
// Query paginated items
|
2024-11-13 01:51:11 +00:00
|
|
|
var rawItems []struct {
|
|
|
|
Path string `db:"path" json:"path"`
|
|
|
|
Count int `db:"count" json:"count"`
|
|
|
|
Last30DaysData string `db:"last_30_days_data" json:"last_30_days_data"` // JSON string to parse
|
2024-11-13 01:04:55 +00:00
|
|
|
}
|
|
|
|
|
2024-11-12 23:31:24 +00:00
|
|
|
err = app.Dao().DB().
|
|
|
|
NewQuery(`
|
2024-11-13 01:32:45 +00:00
|
|
|
SELECT
|
|
|
|
view.path,
|
|
|
|
COUNT(view.id) AS count,
|
2024-11-13 00:42:30 +00:00
|
|
|
(
|
2024-11-12 23:31:24 +00:00
|
|
|
SELECT json_group_array(
|
|
|
|
json_object(
|
2024-11-13 01:12:20 +00:00
|
|
|
'date', date,
|
|
|
|
'count', daily_count
|
2024-11-12 23:31:24 +00:00
|
|
|
)
|
|
|
|
)
|
2024-11-13 01:12:20 +00:00
|
|
|
FROM (
|
2024-11-13 01:32:45 +00:00
|
|
|
SELECT
|
2024-11-13 01:12:20 +00:00
|
|
|
strftime('%Y-%m-%d', created) AS date,
|
|
|
|
COUNT(id) AS daily_count
|
|
|
|
FROM analyticsPageViews
|
2024-11-13 01:32:45 +00:00
|
|
|
WHERE
|
|
|
|
path = view.path
|
|
|
|
AND created >= datetime('now', '-30 days')
|
2024-11-13 01:12:20 +00:00
|
|
|
GROUP BY date
|
|
|
|
) AS daily_data
|
2024-11-12 23:31:24 +00:00
|
|
|
) AS last_30_days_data
|
2024-11-13 01:32:45 +00:00
|
|
|
FROM analyticsPageViews AS view
|
|
|
|
WHERE view.created >= {:startDate}
|
|
|
|
GROUP BY view.path
|
|
|
|
ORDER BY count ` + sort + `
|
2024-11-12 23:35:28 +00:00
|
|
|
LIMIT {:perPage} OFFSET {:offset}
|
2024-11-13 01:32:45 +00:00
|
|
|
`).
|
2024-11-12 23:31:24 +00:00
|
|
|
Bind(dbx.Params{
|
2024-11-13 01:32:45 +00:00
|
|
|
"startDate": startDate.Format("2006-01-02 15:04:05"),
|
2024-11-12 23:35:28 +00:00
|
|
|
"perPage": perPage,
|
|
|
|
"offset": offset,
|
2024-11-12 23:31:24 +00:00
|
|
|
}).
|
2024-11-13 01:51:11 +00:00
|
|
|
All(&rawItems)
|
2024-11-12 23:31:24 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return apis.NewApiError(500, "Failed to query page view data", err)
|
|
|
|
}
|
|
|
|
|
2024-11-13 01:51:11 +00:00
|
|
|
// Parse last_30_days_data JSON string into structured data
|
|
|
|
var items []Item
|
|
|
|
for _, rawItem := range rawItems {
|
|
|
|
var last30DaysData []Last30DaysData
|
|
|
|
if rawItem.Last30DaysData != "" {
|
|
|
|
if err := json.Unmarshal([]byte(rawItem.Last30DaysData), &last30DaysData); err != nil {
|
|
|
|
return apis.NewApiError(500, "Failed to parse last_30_days_data", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
items = append(items, Item{
|
|
|
|
Path: rawItem.Path,
|
|
|
|
Count: rawItem.Count,
|
|
|
|
Last30DaysData: last30DaysData,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-13 01:12:20 +00:00
|
|
|
// Final response structure
|
|
|
|
response := map[string]interface{}{
|
2024-11-13 00:05:14 +00:00
|
|
|
"page": page,
|
|
|
|
"perPage": perPage,
|
|
|
|
"totalItems": totalItems,
|
|
|
|
"totalPages": totalPages,
|
2024-11-13 01:51:11 +00:00
|
|
|
"items": items,
|
2024-11-13 01:12:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return the final JSON response
|
|
|
|
return c.JSON(200, response)
|
2024-11-12 23:31:24 +00:00
|
|
|
}, apis.ActivityLogger(app))
|
|
|
|
}
|
|
|
|
|
|
|
|
// initAggregateCount
|
|
|
|
//
|
|
|
|
// This endpoint is used to aggregate session metadata counts by various categories
|
|
|
|
// (device type, browser name, operating system, user agent, geo country code, and preferred language).
|
|
|
|
// It adds the following endpoint to the app:
|
2024-11-12 22:48:21 +00:00
|
|
|
// GET /api/analytics/sessionCounts
|
|
|
|
//
|
2024-11-12 23:31:24 +00:00
|
|
|
// The endpoint expects the following query parameter:
|
|
|
|
//
|
|
|
|
// ?startDate=ISO 8601 formatted date (optional)
|
|
|
|
//
|
|
|
|
// - `startDate`: An optional start date in ISO 8601 format (e.g., "2024-11-12T15:04:05Z").
|
|
|
|
// If provided, counts reflect sessions from this date onward.
|
|
|
|
// If not provided, counts include all sessions.
|
|
|
|
//
|
|
|
|
// Response format:
|
2024-11-12 22:48:21 +00:00
|
|
|
//
|
|
|
|
// {
|
2024-11-12 23:31:24 +00:00
|
|
|
// "device_type": [
|
2024-11-13 14:38:28 +00:00
|
|
|
// { "value": "<device type>", "count": <session count>, "error_count": <error count>, "success_count": <success count> },
|
2024-11-12 23:31:24 +00:00
|
|
|
// ...
|
|
|
|
// ],
|
|
|
|
// "browser_name": [
|
2024-11-13 14:38:28 +00:00
|
|
|
// { "value": "<browser name>", "count": <session count>, "error_count": <error count>, "success_count": <success count> },
|
2024-11-12 23:31:24 +00:00
|
|
|
// ...
|
|
|
|
// ],
|
|
|
|
// "operating_system": [
|
2024-11-13 14:38:28 +00:00
|
|
|
// { "value": "<operating system>", "count": <session count>, "error_count": <error count>, "success_count": <success count> },
|
2024-11-12 23:31:24 +00:00
|
|
|
// ...
|
|
|
|
// ],
|
|
|
|
// "user_agent": [
|
2024-11-13 14:38:28 +00:00
|
|
|
// { "value": "<user agent>", "count": <session count>, "error_count": <error count>, "success_count": <success count> },
|
2024-11-12 23:31:24 +00:00
|
|
|
// ...
|
|
|
|
// ],
|
|
|
|
// "geo_country_code": [
|
2024-11-13 14:38:28 +00:00
|
|
|
// { "value": "<country code>", "count": <session count>, "error_count": <error count>, "success_count": <success count> },
|
2024-11-12 23:31:24 +00:00
|
|
|
// ...
|
|
|
|
// ],
|
|
|
|
// "preferred_language": [
|
2024-11-13 14:38:28 +00:00
|
|
|
// { "value": "<preferred language>", "count": <session count>, "error_count": <error count>, "success_count": <success count> },
|
2024-11-12 23:31:24 +00:00
|
|
|
// ...
|
|
|
|
// ]
|
2024-11-12 22:48:21 +00:00
|
|
|
// }
|
2024-11-12 23:31:24 +00:00
|
|
|
//
|
2024-11-13 14:16:30 +00:00
|
|
|
// Each category includes an array of objects where `value` is the specific item (e.g., a device type or browser name),
|
|
|
|
// `count` is the total number of sessions matching that item since `startDate`, and `error_count` is the number
|
|
|
|
// of sessions with errors for that item (where the `error` field is not NULL) since `startDate`.
|
2024-11-12 23:31:24 +00:00
|
|
|
func initAggregateCount(app *pocketbase.PocketBase, e *core.ServeEvent) {
|
2024-11-12 23:14:33 +00:00
|
|
|
e.Router.GET("/api/analytics/aggregateCount", func(c echo.Context) error {
|
2024-11-12 22:48:21 +00:00
|
|
|
|
|
|
|
// Check if user is in admin group
|
|
|
|
if err := ldapApi.UserIsInAdminGroup(app, c); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the start date
|
2024-11-12 23:10:17 +00:00
|
|
|
startDate, err := time.Parse(time.RFC3339, c.QueryParam("startDate"))
|
2024-11-12 22:48:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return apis.NewBadRequestError("Invalid start date format, expected ISO 8601", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
type Data struct {
|
2024-11-13 14:38:28 +00:00
|
|
|
Value string `db:"value" json:"value"`
|
|
|
|
Count int `db:"count" json:"count"`
|
|
|
|
SuccessCount int `db:"success_count" json:"success_count"`
|
|
|
|
ErrorCount int `db:"error_count" json:"error_count"`
|
2024-11-12 22:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Use a map to store the response data
|
|
|
|
response := make(map[string][]Data)
|
|
|
|
|
2024-11-12 23:31:24 +00:00
|
|
|
fields := []string{"device_type", "browser_name", "operating_system", "geo_country_code", "preferred_language"}
|
2024-11-12 22:48:21 +00:00
|
|
|
|
|
|
|
for _, field := range fields {
|
|
|
|
data := []Data{}
|
|
|
|
|
|
|
|
err := app.Dao().DB().
|
|
|
|
NewQuery(`
|
2024-11-13 14:16:30 +00:00
|
|
|
SELECT
|
|
|
|
IFNULL(` + field + `, 'N/A') as value,
|
|
|
|
COUNT(id) AS count,
|
2024-11-13 14:38:28 +00:00
|
|
|
SUM(CASE WHEN error IS NULL THEN 1 ELSE 0 END) AS success_count,
|
2024-11-13 14:16:30 +00:00
|
|
|
SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) AS error_count
|
2024-11-13 14:22:23 +00:00
|
|
|
FROM analyticsPageViewsWithSessionDetail
|
2024-11-12 22:48:21 +00:00
|
|
|
WHERE created >= {:startDate}
|
|
|
|
GROUP BY ` + field).
|
|
|
|
Bind(dbx.Params{
|
|
|
|
"startDate": startDate,
|
|
|
|
}).
|
|
|
|
All(&data)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return apis.NewApiError(500, "Failed to aggregate data for "+field, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assign the data to the corresponding field in the map
|
|
|
|
response[field] = data
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the final JSON response
|
|
|
|
return c.JSON(200, response)
|
|
|
|
}, apis.ActivityLogger(app))
|
|
|
|
}
|
|
|
|
|
2024-11-13 19:42:32 +00:00
|
|
|
// initDailyAnalyticsAggregates
|
|
|
|
//
|
|
|
|
// This endpoint aggregates daily analytics data for a specified date range from `startDate` to `now`.
|
|
|
|
// It adds the following endpoint to the app:
|
|
|
|
// GET /api/analytics/dailyAggregates
|
|
|
|
//
|
|
|
|
// The endpoint expects the following query parameter:
|
|
|
|
//
|
|
|
|
// ?startDate=ISO 8601 formatted date (optional)
|
|
|
|
//
|
|
|
|
// - `startDate`: An optional start date in ISO 8601 format (e.g., "2024-11-12T15:04:05Z").
|
|
|
|
// If provided, aggregates are calculated from this date onward up to the current date.
|
|
|
|
// If not provided, all data until today is included.
|
|
|
|
//
|
|
|
|
// Response format:
|
|
|
|
//
|
|
|
|
// [
|
|
|
|
// {
|
|
|
|
// "date": "<ISO 8601>",
|
|
|
|
// "error_count": <error count for that day>,
|
|
|
|
// "page_view_count": <total page views for that day>,
|
|
|
|
// "unique_visitor_count": <unique visitors for that day>
|
|
|
|
// },
|
|
|
|
// ...
|
|
|
|
// ]
|
|
|
|
func initDailyAnalyticsAggregates(app *pocketbase.PocketBase, e *core.ServeEvent) {
|
|
|
|
e.Router.GET("/api/analytics/dailyAggregates", func(c echo.Context) error {
|
|
|
|
|
|
|
|
// Check if user is in admin group
|
|
|
|
if err := ldapApi.UserIsInAdminGroup(app, c); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the start date
|
|
|
|
startDateParam := c.QueryParam("startDate")
|
|
|
|
var startDate time.Time
|
|
|
|
var err error
|
|
|
|
if startDateParam != "" {
|
|
|
|
startDate, err = time.Parse(time.RFC3339, startDateParam)
|
|
|
|
if err != nil {
|
|
|
|
return apis.NewBadRequestError("Invalid start date format, expected ISO 8601", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Default to the beginning of time if not provided
|
|
|
|
startDate = time.Time{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Define the structure for the daily aggregate data
|
|
|
|
type DailyAggregate struct {
|
|
|
|
Date string `db:"date" json:"date"`
|
|
|
|
ErrorCount int `db:"error_count" json:"error_count"`
|
|
|
|
PageViewCount int `db:"page_view_count" json:"page_view_count"`
|
|
|
|
UniqueVisitorCount int `db:"unique_visitor_count" json:"unique_visitor_count"`
|
|
|
|
}
|
|
|
|
|
|
|
|
var results []DailyAggregate
|
|
|
|
|
|
|
|
// Query to aggregate daily data within the specified date range
|
|
|
|
err = app.Dao().DB().
|
|
|
|
NewQuery(`
|
|
|
|
SELECT
|
|
|
|
created AS date,
|
|
|
|
COUNT(id) AS page_view_count,
|
|
|
|
SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) AS error_count,
|
|
|
|
COUNT(DISTINCT visitor) AS unique_visitor_count
|
|
|
|
FROM analyticsPageViewsWithSessionDetail
|
|
|
|
WHERE created >= {:startDate}
|
|
|
|
GROUP BY date
|
|
|
|
ORDER BY date ASC
|
|
|
|
`).
|
|
|
|
Bind(dbx.Params{
|
|
|
|
"startDate": startDate,
|
|
|
|
}).
|
|
|
|
All(&results)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return apis.NewApiError(500, "Failed to query daily analytics aggregates", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the final JSON response
|
|
|
|
return c.JSON(200, results)
|
|
|
|
}, apis.ActivityLogger(app))
|
|
|
|
}
|
|
|
|
|
2024-11-12 22:48:21 +00:00
|
|
|
// InitAnalyticsApi initializes analytics api endpoints
|
|
|
|
func InitAnalyticsApi(app *pocketbase.PocketBase, e *core.ServeEvent) error {
|
|
|
|
|
|
|
|
logger.LogInfoF("Initializing analytics api")
|
|
|
|
|
2024-11-12 23:31:24 +00:00
|
|
|
initPageViewCount(app, e)
|
|
|
|
initAggregateCount(app, e)
|
2024-11-13 19:42:32 +00:00
|
|
|
initDailyAnalyticsAggregates(app, e)
|
2024-11-12 22:48:21 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|