package analyticsApi import ( "encoding/json" "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= // ?perPage= // ?sort= (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": , // "perPage": , // "totalItems": , // "totalPages": , // "items": [ // { // "id": , // "path": "", // "count": , // "last_30_days_data": [ // { // "date": "YYYY-MM-DDTHH:MM:SSZ", // "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) } } else { // Default to the beginning of time if not provided startDate = time.Time{} } // 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 != "ASC" && sort != "DESC" { sort = "DESC" // 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.Format("2006-01-02 15:04:05"), }). 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 { Path string `json:"path"` Count int `json:"count"` Last30DaysData []Last30DaysData `json:"last_30_days_data"` } // Query paginated items 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 } err = app.Dao().DB(). NewQuery(` SELECT view.path, COUNT(view.id) 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 AS view WHERE view.created >= {:startDate} GROUP BY view.path ORDER BY count ` + sort + ` LIMIT {:perPage} OFFSET {:offset} `). Bind(dbx.Params{ "startDate": startDate.Format("2006-01-02 15:04:05"), "perPage": perPage, "offset": offset, }). All(&rawItems) if err != nil { return apis.NewApiError(500, "Failed to query page view data", err) } // 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, }) } // 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": "", "count": , "error_count": , "success_count": }, // ... // ], // "browser_name": [ // { "value": "", "count": , "error_count": , "success_count": }, // ... // ], // "operating_system": [ // { "value": "", "count": , "error_count": , "success_count": }, // ... // ], // "user_agent": [ // { "value": "", "count": , "error_count": , "success_count": }, // ... // ], // "geo_country_code": [ // { "value": "", "count": , "error_count": , "success_count": }, // ... // ], // "preferred_language": [ // { "value": "", "count": , "error_count": , "success_count": }, // ... // ] // } // // 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`. func initAggregateCount(app *pocketbase.PocketBase, e *core.ServeEvent) { 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"` SuccessCount int `db:"success_count" json:"success_count"` ErrorCount int `db:"error_count" json:"error_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(` SELECT IFNULL(` + field + `, 'N/A') as value, COUNT(id) AS count, SUM(CASE WHEN error IS NULL THEN 1 ELSE 0 END) AS success_count, SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) AS error_count FROM analyticsPageViewsWithSessionDetail 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 }