stuve-it-backend/analyticsApi/main.go

297 lines
7.7 KiB
Go
Raw Normal View History

package analyticsApi
import (
"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"
"strconv"
"time"
)
// initPageViewCount
//
// This endpoint is used to retrieve paginated page view counts for each unique path.
// It adds the following endpoint to the app:
// GET /api/analytics/pageViewCount
//
// Query parameters:
//
// ?startDate=ISO 8601 formatted date (optional)
// ?page=<page number>
// ?perPage=<results per page>
// ?sort=<ASC|DESC> (default is ASC)
//
// - `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).
//
// Response format:
//
// {
// "page": <current page number>,
// "perPage": <results per page>,
// "totalItems": <total number of items matching the filter>,
// "totalPages": <total number of pages>,
// "items": [
// {
// "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>
// },
// ...
// ]
// },
// ...
// ]
// }
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)
}
}
// Pagination and sorting parameters
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")
if sort != "DESC" {
sort = "ASC" // default sorting order
}
offset := (page - 1) * perPage
// 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{
"startDate": startDate,
}).
Row(&totalItems)
if err != nil {
return apis.NewApiError(500, "Failed to count total items", err)
}
totalPages := (totalItems + perPage - 1) / perPage // Calculate total pages
// Define the structure for items
type Last30DaysData struct {
Date string `json:"date"`
Count int `json:"count"`
}
type Item struct {
ID int `json:"id"`
Path string `json:"path"`
Count int `json:"count"`
Last30DaysData []Last30DaysData `json:"last_30_days_data"`
}
2024-11-13 00:14:18 +00:00
// Query paginated items, sorted by count
var items []Item
err = app.Dao().DB().
NewQuery(`
SELECT
(ROW_NUMBER() OVER()) AS id,
view.path AS path,
(
SELECT COUNT(id)
FROM analyticsPageViews
WHERE path = view.path AND created >= {:startDate}
) AS count,
(
SELECT json_group_array(
json_object(
'date', date,
'count', daily_count
)
)
FROM (
SELECT
strftime('%Y-%m-%d', created) AS date,
COUNT(id) AS daily_count
FROM analyticsPageViews
WHERE
path = view.path AND
created >= datetime('now', '-30 days')
GROUP BY date
) AS daily_data
) AS last_30_days_data
FROM
analyticsPageViews view
GROUP BY
view.path
ORDER BY
2024-11-13 00:14:18 +00:00
count ` + sort + `
LIMIT {:perPage} OFFSET {:offset}
`).
Bind(dbx.Params{
"startDate": startDate,
"perPage": perPage,
"offset": offset,
}).
All(&items)
if err != nil {
return apis.NewApiError(500, "Failed to query page view data", err)
}
// Final response structure
response := map[string]interface{}{
"page": page,
"perPage": perPage,
"totalItems": totalItems,
"totalPages": totalPages,
"items": items,
}
// Return the final JSON response
return c.JSON(200, response)
}, 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:
// GET /api/analytics/sessionCounts
//
// 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:
//
// {
// "device_type": [
// { "value": "<device type>", "count": <session count> },
// ...
// ],
// "browser_name": [
// { "value": "<browser name>", "count": <session count> },
// ...
// ],
// "operating_system": [
// { "value": "<operating system>", "count": <session count> },
// ...
// ],
// "user_agent": [
// { "value": "<user agent>", "count": <session count> },
// ...
// ],
// "geo_country_code": [
// { "value": "<country code>", "count": <session count> },
// ...
// ],
// "preferred_language": [
// { "value": "<preferred language>", "count": <session count> },
// ...
// ]
// }
//
// Each category includes an array of objects where `value` is the specific item (e.g., a device type or browser name)
// and `count` is the number of sessions matching that item since `startDate`.
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 {
// Check if user is in admin group
if err := ldapApi.UserIsInAdminGroup(app, c); err != nil {
return err
}
// Parse the start date
startDate, err := time.Parse(time.RFC3339, c.QueryParam("startDate"))
if err != nil {
return apis.NewBadRequestError("Invalid start date format, expected ISO 8601", err)
}
type Data struct {
Value string `db:"value" json:"value"`
Count int `db:"count" json:"count"`
}
// Use a map to store the response data
response := make(map[string][]Data)
fields := []string{"device_type", "browser_name", "operating_system", "geo_country_code", "preferred_language"}
for _, field := range fields {
data := []Data{}
err := app.Dao().DB().
NewQuery(`
2024-11-12 23:50:52 +00:00
SELECT IFNULL(` + field + `, 'N/A') as value, COUNT(id) AS count
FROM analyticsSessions
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))
}
// InitAnalyticsApi initializes analytics api endpoints
func InitAnalyticsApi(app *pocketbase.PocketBase, e *core.ServeEvent) error {
logger.LogInfoF("Initializing analytics api")
initPageViewCount(app, e)
initAggregateCount(app, e)
return nil
}