improve on the subimsion process with automatic submissions
make sure we give each user uniqe id
This commit is contained in:
@@ -169,6 +169,8 @@ func commandNeedsApp(cmd *cobra.Command) bool {
|
|||||||
|
|
||||||
func ensureAppInitialized(ctx context.Context) error {
|
func ensureAppInitialized(ctx context.Context) error {
|
||||||
if App != nil {
|
if App != nil {
|
||||||
|
survey.SetApp(App)
|
||||||
|
issues.SetApp(App)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/cmd/pm/tasks"
|
"github.com/LazyBachelor/LazyPM/cmd/pm/tasks"
|
||||||
"github.com/LazyBachelor/LazyPM/internal/commands/survey"
|
"github.com/LazyBachelor/LazyPM/internal/commands/survey"
|
||||||
|
"github.com/LazyBachelor/LazyPM/internal/storage"
|
||||||
"github.com/LazyBachelor/LazyPM/pkg/task"
|
"github.com/LazyBachelor/LazyPM/pkg/task"
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +31,54 @@ func runStartCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("application services are not available")
|
return fmt.Errorf("application services are not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
var noSubmit bool
|
||||||
|
cmd.Println("No MongoDB password provided, survey responses will not be submitted.")
|
||||||
|
cmd.Println("you can manually submit later with `pm survey submit <mongo-password>`.")
|
||||||
|
|
||||||
|
huh.NewConfirm().Title("Are you sure you want to continue without submiting data?").
|
||||||
|
Value(&noSubmit).RunAccessible(cmd.OutOrStdout(), cmd.InOrStdin())
|
||||||
|
|
||||||
|
if !noSubmit {
|
||||||
|
cmd.Println("Exiting. Please run the command again with a MongoDB password to submit your data.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mongoPass := args[0]
|
||||||
|
mongoStorage, err := storage.NewMongoStorage(app.Config.MongoURI, "participant", mongoPass)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Println("Failed to connect to MongoDB, survey responses will not be submitted.")
|
||||||
|
cmd.Println("You can manually submit later with `pm survey submit <password>`.")
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
cmd.Println("Connected to MongoDB successfully.")
|
||||||
|
|
||||||
|
defer mongoStorage.Close()
|
||||||
|
|
||||||
|
ctx := cmd.Context()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := mongoStorage.SubmitSurveyResponsesCmd(ctx); err != nil {
|
||||||
|
cmd.Printf("Failed to submit survey responses: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := mongoStorage.SubmitSurveyResponsesCmd(ctx); err != nil {
|
||||||
|
cmd.Printf("Failed to submit survey responses: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interfaces := initInterfaces()
|
interfaces := initInterfaces()
|
||||||
surveyTasks := initTasks(app)
|
surveyTasks := initTasks(app)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||||
"github.com/LazyBachelor/LazyPM/internal/storage"
|
"github.com/LazyBachelor/LazyPM/internal/storage"
|
||||||
"github.com/steveyegge/beads"
|
"github.com/steveyegge/beads"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App = models.App
|
type App = models.App
|
||||||
@@ -42,7 +43,7 @@ func New(ctx context.Context, config Config, opts ...Option) (*App, func(), erro
|
|||||||
|
|
||||||
if b.statsService == nil {
|
if b.statsService == nil {
|
||||||
statStore := storage.NewJsonStorage(config.StatisticsStoragePath, &models.Statistics{
|
statStore := storage.NewJsonStorage(config.StatisticsStoragePath, &models.Statistics{
|
||||||
ID: 0,
|
ID: primitive.NewObjectID(),
|
||||||
StartTime: time.Now(),
|
StartTime: time.Now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||||
"github.com/LazyBachelor/LazyPM/internal/storage"
|
"github.com/LazyBachelor/LazyPM/internal/storage"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatisticsService struct {
|
type StatisticsService struct {
|
||||||
@@ -42,6 +43,16 @@ func (s *StatisticsService) GetStatistics() (models.Statistics, error) {
|
|||||||
return *s.storage.Data, nil
|
return *s.storage.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StatisticsService) GetParticipantID() primitive.ObjectID {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.storage.Data == nil {
|
||||||
|
return primitive.NilObjectID
|
||||||
|
}
|
||||||
|
return s.storage.Data.ID
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StatisticsService) RecordTaskRun(ctx context.Context, run models.TaskRunMetrics) error {
|
func (s *StatisticsService) RecordTaskRun(ctx context.Context, run models.TaskRunMetrics) error {
|
||||||
_ = ctx
|
_ = ctx
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ Your responses will be kept confidential and used solely for research purposes.`
|
|||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
Use: "survey",
|
Use: "survey",
|
||||||
Long: long,
|
Long: long,
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
if app != nil {
|
|
||||||
cmd.SetContext(context.WithValue(cmd.Context(), appKey, app))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetApp(application *App) {
|
func SetApp(application *App) {
|
||||||
|
|||||||
@@ -1,123 +1,40 @@
|
|||||||
package survey
|
package survey
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
"github.com/LazyBachelor/LazyPM/internal/storage"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var mongoURI = os.Getenv("MONGODB_URI")
|
|
||||||
|
|
||||||
var SubmitCmd = &cobra.Command{
|
var SubmitCmd = &cobra.Command{
|
||||||
Use: "submit",
|
Use: "submit <mongo-password>",
|
||||||
Short: "Submit your survey responses",
|
Short: "Submit your survey responses",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, error := mongo.Connect(cmd.Context(), options.Client().ApplyURI(mongoURI))
|
app := AppFromContext(cmd.Context())
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("Failed to connect to MongoDB: %v", error)
|
if app == nil {
|
||||||
|
return fmt.Errorf("application context not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
if len(args) == 0 {
|
||||||
if err := client.Disconnect(cmd.Context()); err != nil {
|
cmd.Println("No MongoDB password provided, survey responses will not be submitted.")
|
||||||
fmt.Printf("Failed to disconnect MongoDB client: %v", err)
|
return nil
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
userStatscollection := client.Database("Responses").Collection("stats")
|
mongoPassword := args[0]
|
||||||
taskMetricsCollection := client.Database("Responses").Collection("task_metrics")
|
mongoClient, err := storage.NewMongoStorage(app.Config.MongoURI, "participant", mongoPassword)
|
||||||
|
|
||||||
pmDir := "./.pm/"
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(pmDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to read .pm directory: %v", err)
|
return fmt.Errorf("failed to connect to MongoDB: %w", err)
|
||||||
|
}
|
||||||
|
defer mongoClient.Close()
|
||||||
|
|
||||||
|
if err := mongoClient.SubmitSurveyResponsesCmd(cmd.Context()); err != nil {
|
||||||
|
return fmt.Errorf("failed to submit survey responses: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(entries) == 0 {
|
cmd.Println("Successfully submitted survey responses and metrics to the database")
|
||||||
return fmt.Errorf("No files found in .pm directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
statFile := pmDir + "stats.json"
|
|
||||||
if _, err := os.Stat(statFile); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("stats.json not found in .pm directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
stats, err := getStats(statFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to read stats.json: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = userStatscollection.InsertOne(cmd.Context(), stats)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to insert stats into database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
metricFiles := []string{}
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.Name() == "stats.json" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(entry.Name(), "-stats.json") {
|
|
||||||
metricFiles = append(metricFiles, pmDir+entry.Name())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range metricFiles {
|
|
||||||
metrics, err := getTaskMetrics(file)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to read metrics from %s: %v", file, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = taskMetricsCollection.InsertOne(cmd.Context(), metrics)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to insert metrics from %s: %v", file, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("Successfully submitted survey responses and metrics to the database")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStats(file string) (*models.Statistics, error) {
|
|
||||||
data, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var stats models.Statistics
|
|
||||||
if err := json.Unmarshal(data, &stats); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTaskMetrics(file string) (*models.TaskMetricsFile, error) {
|
|
||||||
data, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var metrics models.TaskMetricsFile
|
|
||||||
if err := json.Unmarshal(data, &metrics); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &metrics, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package models
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
@@ -57,5 +59,6 @@ type StatsService interface {
|
|||||||
Load(ctx context.Context) error
|
Load(ctx context.Context) error
|
||||||
Save(ctx context.Context) error
|
Save(ctx context.Context) error
|
||||||
GetStatistics() (Statistics, error)
|
GetStatistics() (Statistics, error)
|
||||||
|
GetParticipantID() primitive.ObjectID
|
||||||
RecordTaskRun(ctx context.Context, run TaskRunMetrics) error
|
RecordTaskRun(ctx context.Context, run TaskRunMetrics) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
MongoURI string
|
||||||
AutoInit bool
|
AutoInit bool
|
||||||
RootCmd string
|
RootCmd string
|
||||||
WebAddress string
|
WebAddress string
|
||||||
@@ -11,6 +12,7 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var BaseConfig = Config{
|
var BaseConfig = Config{
|
||||||
|
MongoURI: "mongodb+srv://lazy.wf9kdi8.mongodb.net/",
|
||||||
AutoInit: false,
|
AutoInit: false,
|
||||||
RootCmd: "pm",
|
RootCmd: "pm",
|
||||||
IssuePrefix: "pm",
|
IssuePrefix: "pm",
|
||||||
@@ -19,6 +21,11 @@ var BaseConfig = Config{
|
|||||||
StatisticsStoragePath: "./.pm/stats.json",
|
StatisticsStoragePath: "./.pm/stats.json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Config) WithMongoURI(uri string) Config {
|
||||||
|
c.MongoURI = uri
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
func (c Config) WithAutoInit(autoInit bool) Config {
|
func (c Config) WithAutoInit(autoInit bool) Config {
|
||||||
c.AutoInit = autoInit
|
c.AutoInit = autoInit
|
||||||
return c
|
return c
|
||||||
|
|||||||
@@ -2,91 +2,95 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Statistics struct {
|
type Statistics struct {
|
||||||
ID int `json:"id"`
|
ID primitive.ObjectID `bson:"_id" json:"id"`
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime time.Time `bson:"start_time" json:"start_time"`
|
||||||
EndTime time.Time `json:"end_time"`
|
EndTime time.Time `bson:"end_time" json:"end_time"`
|
||||||
Duration time.Duration `json:"duration"`
|
Duration time.Duration `bson:"duration" json:"duration"`
|
||||||
|
|
||||||
InterfaceType InterfaceType `json:"interface_type"`
|
InterfaceType InterfaceType `bson:"interface_type" json:"interface_type"`
|
||||||
TaskRuns int `json:"task_runs"`
|
TaskRuns int `bson:"task_runs" json:"task_runs"`
|
||||||
TasksCompleted int `json:"tasks_completed"`
|
TasksCompleted int `bson:"tasks_completed" json:"tasks_completed"`
|
||||||
TasksFailed int `json:"tasks_failed"`
|
TasksFailed int `bson:"tasks_failed" json:"tasks_failed"`
|
||||||
|
|
||||||
TotalDurationMs int64 `json:"total_duration_ms"`
|
TotalDurationMs int64 `bson:"total_duration_ms" json:"total_duration_ms"`
|
||||||
AverageDurationMs int64 `json:"average_duration_ms"`
|
AverageDurationMs int64 `bson:"average_duration_ms" json:"average_duration_ms"`
|
||||||
|
|
||||||
TotalUserActions int `json:"total_user_actions"`
|
TotalUserActions int `bson:"total_user_actions" json:"total_user_actions"`
|
||||||
QuestionnairesCompleted int `json:"questionnaires_completed"`
|
QuestionnairesCompleted int `bson:"questionnaires_completed" json:"questionnaires_completed"`
|
||||||
QuestionnairesAbandoned int `json:"questionnaires_abandoned"`
|
QuestionnairesAbandoned int `bson:"questionnaires_abandoned" json:"questionnaires_abandoned"`
|
||||||
|
|
||||||
ValidationAttempts int `json:"validation_attempts"`
|
ValidationAttempts int `bson:"validation_attempts" json:"validation_attempts"`
|
||||||
ValidationSuccesses int `json:"validation_successes"`
|
ValidationSuccesses int `bson:"validation_successes" json:"validation_successes"`
|
||||||
ValidationFailures int `json:"validation_failures"`
|
ValidationFailures int `bson:"validation_failures" json:"validation_failures"`
|
||||||
ValidationChecksPassed int `json:"validation_checks_passed"`
|
ValidationChecksPassed int `bson:"validation_checks_passed" json:"validation_checks_passed"`
|
||||||
ValidationChecksFailed int `json:"validation_checks_failed"`
|
ValidationChecksFailed int `bson:"validation_checks_failed" json:"validation_checks_failed"`
|
||||||
|
|
||||||
LastTaskName string `json:"last_task_name"`
|
LastTaskName string `bson:"last_task_name" json:"last_task_name"`
|
||||||
LastRunID int `json:"last_run_id"`
|
LastRunID int `bson:"last_run_id" json:"last_run_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskMetricsFile struct {
|
type TaskMetricsFile struct {
|
||||||
TaskName string `json:"task_name"`
|
ID primitive.ObjectID `bson:"_id" json:"id"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
ParticipantID primitive.ObjectID `bson:"participant_id" json:"participant_id"`
|
||||||
Summary TaskStatsSummary `json:"summary"`
|
TaskName string `bson:"task_name" json:"task_name"`
|
||||||
Runs []TaskRunMetrics `json:"runs"`
|
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
|
||||||
|
Summary TaskStatsSummary `bson:"summary" json:"summary"`
|
||||||
|
Runs []TaskRunMetrics `bson:"runs" json:"runs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskStatsSummary struct {
|
type TaskStatsSummary struct {
|
||||||
TotalRuns int `json:"total_runs"`
|
TotalRuns int `bson:"total_runs" json:"total_runs"`
|
||||||
CompletedRuns int `json:"completed_runs"`
|
CompletedRuns int `bson:"completed_runs" json:"completed_runs"`
|
||||||
IncompleteRuns int `json:"incomplete_runs"`
|
IncompleteRuns int `bson:"incomplete_runs" json:"incomplete_runs"`
|
||||||
TotalDurationMs int64 `json:"total_duration_ms"`
|
TotalDurationMs int64 `bson:"total_duration_ms" json:"total_duration_ms"`
|
||||||
AverageDurationMs int64 `json:"average_duration_ms"`
|
AverageDurationMs int64 `bson:"average_duration_ms" json:"average_duration_ms"`
|
||||||
TotalUserActions int `json:"total_user_actions"`
|
TotalUserActions int `bson:"total_user_actions" json:"total_user_actions"`
|
||||||
QuestionnairesCompleted int `json:"questionnaires_completed"`
|
QuestionnairesCompleted int `bson:"questionnaires_completed" json:"questionnaires_completed"`
|
||||||
QuestionnairesAbandoned int `json:"questionnaires_abandoned"`
|
QuestionnairesAbandoned int `bson:"questionnaires_abandoned" json:"questionnaires_abandoned"`
|
||||||
ValidationAttempts int `json:"validation_attempts"`
|
ValidationAttempts int `bson:"validation_attempts" json:"validation_attempts"`
|
||||||
ValidationSuccesses int `json:"validation_successes"`
|
ValidationSuccesses int `bson:"validation_successes" json:"validation_successes"`
|
||||||
ValidationFailures int `json:"validation_failures"`
|
ValidationFailures int `bson:"validation_failures" json:"validation_failures"`
|
||||||
ValidationChecksPassed int `json:"validation_checks_passed"`
|
ValidationChecksPassed int `bson:"validation_checks_passed" json:"validation_checks_passed"`
|
||||||
ValidationChecksFailed int `json:"validation_checks_failed"`
|
ValidationChecksFailed int `bson:"validation_checks_failed" json:"validation_checks_failed"`
|
||||||
LastInterfaceType InterfaceType `json:"last_interface_type"`
|
LastInterfaceType InterfaceType `bson:"last_interface_type" json:"last_interface_type"`
|
||||||
FirstRunStartedAt time.Time `json:"first_run_started_at"`
|
FirstRunStartedAt time.Time `bson:"first_run_started_at" json:"first_run_started_at"`
|
||||||
LastRunStartedAt time.Time `json:"last_run_started_at"`
|
LastRunStartedAt time.Time `bson:"last_run_started_at" json:"last_run_started_at"`
|
||||||
LastRunEndedAt time.Time `json:"last_run_ended_at"`
|
LastRunEndedAt time.Time `bson:"last_run_ended_at" json:"last_run_ended_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskRunMetrics struct {
|
type TaskRunMetrics struct {
|
||||||
RunID int `json:"run_id"`
|
RunID int `bson:"run_id" json:"run_id"`
|
||||||
TaskName string `json:"task_name"`
|
TaskName string `bson:"task_name" json:"task_name"`
|
||||||
InterfaceType InterfaceType `json:"interface_type"`
|
InterfaceType InterfaceType `bson:"interface_type" json:"interface_type"`
|
||||||
StartedAt time.Time `json:"started_at"`
|
StartedAt time.Time `bson:"started_at" json:"started_at"`
|
||||||
EndedAt time.Time `json:"ended_at"`
|
EndedAt time.Time `bson:"ended_at" json:"ended_at"`
|
||||||
DurationMs int64 `json:"duration_ms"`
|
DurationMs int64 `bson:"duration_ms" json:"duration_ms"`
|
||||||
Completed bool `json:"completed"`
|
Completed bool `bson:"completed" json:"completed"`
|
||||||
ValidationAttempts int `json:"validation_attempts"`
|
ValidationAttempts int `bson:"validation_attempts" json:"validation_attempts"`
|
||||||
ValidationSuccesses int `json:"validation_successes"`
|
ValidationSuccesses int `bson:"validation_successes" json:"validation_successes"`
|
||||||
ValidationFailures int `json:"validation_failures"`
|
ValidationFailures int `bson:"validation_failures" json:"validation_failures"`
|
||||||
ValidationChecksPassed int `json:"validation_checks_passed"`
|
ValidationChecksPassed int `bson:"validation_checks_passed" json:"validation_checks_passed"`
|
||||||
ValidationChecksFailed int `json:"validation_checks_failed"`
|
ValidationChecksFailed int `bson:"validation_checks_failed" json:"validation_checks_failed"`
|
||||||
LastValidationMessage string `json:"last_validation_message,omitempty"`
|
LastValidationMessage string `bson:"last_validation_message,omitempty" json:"last_validation_message,omitempty"`
|
||||||
QuestionnaireCompleted bool `json:"questionnaire_completed"`
|
QuestionnaireCompleted bool `bson:"questionnaire_completed" json:"questionnaire_completed"`
|
||||||
QuestionnaireUserQuit bool `json:"questionnaire_user_quit"`
|
QuestionnaireUserQuit bool `bson:"questionnaire_user_quit" json:"questionnaire_user_quit"`
|
||||||
QuestionnaireAnswers map[string]any `json:"questionnaire_answers,omitempty"`
|
QuestionnaireAnswers map[string]any `bson:"questionnaire_answers,omitempty" json:"questionnaire_answers,omitempty"`
|
||||||
Logs []TaskLogEntry `json:"logs"`
|
Logs []TaskLogEntry `bson:"logs" json:"logs"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `bson:"error,omitempty" json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskLogEntry struct {
|
type TaskLogEntry struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `bson:"timestamp" json:"timestamp"`
|
||||||
Level string `json:"level"`
|
Level string `bson:"level" json:"level"`
|
||||||
Message string `json:"message"`
|
Message string `bson:"message" json:"message"`
|
||||||
Source string `json:"source,omitempty"`
|
Source string `bson:"source,omitempty" json:"source,omitempty"`
|
||||||
Action string `json:"action,omitempty"`
|
Action string `bson:"action,omitempty" json:"action,omitempty"`
|
||||||
Target string `json:"target,omitempty"`
|
Target string `bson:"target,omitempty" json:"target,omitempty"`
|
||||||
Result string `json:"result,omitempty"`
|
Result string `bson:"result,omitempty" json:"result,omitempty"`
|
||||||
Attempt int `json:"attempt,omitempty"`
|
Attempt int `bson:"attempt,omitempty" json:"attempt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
136
internal/storage/mongo.go
Normal file
136
internal/storage/mongo.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MongoStorage struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMongoStorage(uri, username, password string) (*MongoStorage, error) {
|
||||||
|
credentials := options.Credential{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(uri).SetAuth(credentials))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to MongoDB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Ping(context.Background(), nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot reach MongoDB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MongoStorage{client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MongoStorage) Close() error {
|
||||||
|
return s.client.Disconnect(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MongoStorage) SubmitSurveyResponsesCmd(ctx context.Context) error {
|
||||||
|
pmDir := "./.pm/"
|
||||||
|
userStatscollection := s.client.Database("Responses").Collection("stats")
|
||||||
|
taskMetricsCollection := s.client.Database("Responses").Collection("metrics")
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(pmDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to read .pm directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("No files found in .pm directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
statFile := pmDir + "stats.json"
|
||||||
|
if _, err := os.Stat(statFile); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("stats.json not found in .pm directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := getStats(statFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to read stats.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = userStatscollection.UpdateOne(ctx,
|
||||||
|
bson.M{"_id": stats.ID},
|
||||||
|
bson.M{"$set": stats},
|
||||||
|
options.Update().SetUpsert(true))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to insert stats into database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metricFiles := []string{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Name() == "stats.json" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(entry.Name(), "-stats.json") {
|
||||||
|
metricFiles = append(metricFiles, pmDir+entry.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range metricFiles {
|
||||||
|
metrics, err := getTaskMetrics(file)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to read metrics from %s: %v", file, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = taskMetricsCollection.UpdateOne(ctx, bson.M{"_id": metrics.ID}, bson.M{"$set": metrics},
|
||||||
|
options.Update().SetUpsert(true))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to insert metrics from %s: %v", file, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStats(file string) (*models.Statistics, error) {
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats models.Statistics
|
||||||
|
if err := json.Unmarshal(data, &stats); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTaskMetrics(file string) (*models.TaskMetricsFile, error) {
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var metrics models.TaskMetricsFile
|
||||||
|
if err := json.Unmarshal(data, &metrics); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &metrics, nil
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RunLifecycle struct {
|
type RunLifecycle struct {
|
||||||
@@ -23,9 +24,11 @@ func NewRunLifecycle(app *App, config Config, details models.TaskDetails, iType
|
|||||||
collector.recordUserAction(action)
|
collector.recordUserAction(action)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var participantID primitive.ObjectID
|
||||||
var store MetricsStore
|
var store MetricsStore
|
||||||
if config.StatisticsStoragePath != "" {
|
if config.StatisticsStoragePath != "" {
|
||||||
store = NewFileMetricsStore(config.StatisticsStoragePath, logger)
|
participantID = app.Stats.GetParticipantID()
|
||||||
|
store = NewFileMetricsStore(config.StatisticsStoragePath, participantID, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &RunLifecycle{
|
return &RunLifecycle{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetricsStore interface {
|
type MetricsStore interface {
|
||||||
@@ -17,14 +18,16 @@ type MetricsStore interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileMetricsStore struct {
|
type FileMetricsStore struct {
|
||||||
path string
|
path string
|
||||||
logger *slog.Logger
|
participantID primitive.ObjectID
|
||||||
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileMetricsStore(path string, logger *slog.Logger) *FileMetricsStore {
|
func NewFileMetricsStore(path string, participantID primitive.ObjectID, logger *slog.Logger) *FileMetricsStore {
|
||||||
return &FileMetricsStore{
|
return &FileMetricsStore{
|
||||||
path: path,
|
path: path,
|
||||||
logger: logger,
|
participantID: participantID,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +44,10 @@ func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
metrics := models.TaskMetricsFile{
|
metrics := models.TaskMetricsFile{
|
||||||
TaskName: taskName,
|
ID: primitive.NewObjectID(),
|
||||||
Runs: []models.TaskRunMetrics{},
|
ParticipantID: s.participantID,
|
||||||
|
TaskName: taskName,
|
||||||
|
Runs: []models.TaskRunMetrics{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := readMetrics(&metrics, s.path); err != nil {
|
if err := readMetrics(&metrics, s.path); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user