diff --git a/cmd/pm/init.go b/cmd/pm/init.go index 5ba5040..1f69a94 100644 --- a/cmd/pm/init.go +++ b/cmd/pm/init.go @@ -169,6 +169,8 @@ func commandNeedsApp(cmd *cobra.Command) bool { func ensureAppInitialized(ctx context.Context) error { if App != nil { + survey.SetApp(App) + issues.SetApp(App) return nil } diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index 1c14bae..cb786ef 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -5,10 +5,13 @@ import ( "errors" "fmt" "math/rand" + "time" "github.com/LazyBachelor/LazyPM/cmd/pm/tasks" "github.com/LazyBachelor/LazyPM/internal/commands/survey" + "github.com/LazyBachelor/LazyPM/internal/storage" "github.com/LazyBachelor/LazyPM/pkg/task" + "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) @@ -28,6 +31,54 @@ func runStartCmd(cmd *cobra.Command, args []string) error { 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 `.") + + 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 `.") + 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() surveyTasks := initTasks(app) diff --git a/internal/app/app.go b/internal/app/app.go index b44afa2..2705c97 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,6 +7,7 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" "github.com/steveyegge/beads" + "go.mongodb.org/mongo-driver/bson/primitive" ) type App = models.App @@ -42,7 +43,7 @@ func New(ctx context.Context, config Config, opts ...Option) (*App, func(), erro if b.statsService == nil { statStore := storage.NewJsonStorage(config.StatisticsStoragePath, &models.Statistics{ - ID: 0, + ID: primitive.NewObjectID(), StartTime: time.Now(), }) diff --git a/internal/app/statistics.go b/internal/app/statistics.go index dadc342..ae49fb8 100644 --- a/internal/app/statistics.go +++ b/internal/app/statistics.go @@ -9,6 +9,7 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" + "go.mongodb.org/mongo-driver/bson/primitive" ) type StatisticsService struct { @@ -42,6 +43,16 @@ func (s *StatisticsService) GetStatistics() (models.Statistics, error) { 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 { _ = ctx diff --git a/internal/commands/survey/root.go b/internal/commands/survey/root.go index 41a4d21..f148f08 100644 --- a/internal/commands/survey/root.go +++ b/internal/commands/survey/root.go @@ -26,11 +26,6 @@ Your responses will be kept confidential and used solely for research purposes.` var RootCmd = &cobra.Command{ Use: "survey", Long: long, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - if app != nil { - cmd.SetContext(context.WithValue(cmd.Context(), appKey, app)) - } - }, } func SetApp(application *App) { diff --git a/internal/commands/survey/submit.go b/internal/commands/survey/submit.go index 8d0a7b2..da80453 100644 --- a/internal/commands/survey/submit.go +++ b/internal/commands/survey/submit.go @@ -1,123 +1,40 @@ package survey import ( - "encoding/json" "fmt" - "os" - "strings" - "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/storage" "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{ - Use: "submit", + Use: "submit ", Short: "Submit your survey responses", + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, error := mongo.Connect(cmd.Context(), options.Client().ApplyURI(mongoURI)) - if error != nil { - return fmt.Errorf("Failed to connect to MongoDB: %v", error) + app := AppFromContext(cmd.Context()) + + if app == nil { + return fmt.Errorf("application context not initialized") } - go func() { - if err := client.Disconnect(cmd.Context()); err != nil { - fmt.Printf("Failed to disconnect MongoDB client: %v", err) - } - }() + if len(args) == 0 { + cmd.Println("No MongoDB password provided, survey responses will not be submitted.") + return nil + } - userStatscollection := client.Database("Responses").Collection("stats") - taskMetricsCollection := client.Database("Responses").Collection("task_metrics") - - pmDir := "./.pm/" - - entries, err := os.ReadDir(pmDir) + mongoPassword := args[0] + mongoClient, err := storage.NewMongoStorage(app.Config.MongoURI, "participant", mongoPassword) 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 { - 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") - + cmd.Println("Successfully submitted survey responses and metrics to the database") 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 -} diff --git a/internal/models/app.go b/internal/models/app.go index 5ba33b1..9b81226 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -3,6 +3,8 @@ package models import ( "context" "log/slog" + + "go.mongodb.org/mongo-driver/bson/primitive" ) type App struct { @@ -57,5 +59,6 @@ type StatsService interface { Load(ctx context.Context) error Save(ctx context.Context) error GetStatistics() (Statistics, error) + GetParticipantID() primitive.ObjectID RecordTaskRun(ctx context.Context, run TaskRunMetrics) error } diff --git a/internal/models/config.go b/internal/models/config.go index 48a7f62..34fff5c 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -1,6 +1,7 @@ package models type Config struct { + MongoURI string AutoInit bool RootCmd string WebAddress string @@ -11,6 +12,7 @@ type Config struct { } var BaseConfig = Config{ + MongoURI: "mongodb+srv://lazy.wf9kdi8.mongodb.net/", AutoInit: false, RootCmd: "pm", IssuePrefix: "pm", @@ -19,6 +21,11 @@ var BaseConfig = Config{ StatisticsStoragePath: "./.pm/stats.json", } +func (c Config) WithMongoURI(uri string) Config { + c.MongoURI = uri + return c +} + func (c Config) WithAutoInit(autoInit bool) Config { c.AutoInit = autoInit return c diff --git a/internal/models/statistics.go b/internal/models/statistics.go index b151072..e95d8f1 100644 --- a/internal/models/statistics.go +++ b/internal/models/statistics.go @@ -2,91 +2,95 @@ package models import ( "time" + + "go.mongodb.org/mongo-driver/bson/primitive" ) type Statistics struct { - ID int `json:"id"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - Duration time.Duration `json:"duration"` + ID primitive.ObjectID `bson:"_id" json:"id"` + StartTime time.Time `bson:"start_time" json:"start_time"` + EndTime time.Time `bson:"end_time" json:"end_time"` + Duration time.Duration `bson:"duration" json:"duration"` - InterfaceType InterfaceType `json:"interface_type"` - TaskRuns int `json:"task_runs"` - TasksCompleted int `json:"tasks_completed"` - TasksFailed int `json:"tasks_failed"` + InterfaceType InterfaceType `bson:"interface_type" json:"interface_type"` + TaskRuns int `bson:"task_runs" json:"task_runs"` + TasksCompleted int `bson:"tasks_completed" json:"tasks_completed"` + TasksFailed int `bson:"tasks_failed" json:"tasks_failed"` - TotalDurationMs int64 `json:"total_duration_ms"` - AverageDurationMs int64 `json:"average_duration_ms"` + TotalDurationMs int64 `bson:"total_duration_ms" json:"total_duration_ms"` + AverageDurationMs int64 `bson:"average_duration_ms" json:"average_duration_ms"` - TotalUserActions int `json:"total_user_actions"` - QuestionnairesCompleted int `json:"questionnaires_completed"` - QuestionnairesAbandoned int `json:"questionnaires_abandoned"` + TotalUserActions int `bson:"total_user_actions" json:"total_user_actions"` + QuestionnairesCompleted int `bson:"questionnaires_completed" json:"questionnaires_completed"` + QuestionnairesAbandoned int `bson:"questionnaires_abandoned" json:"questionnaires_abandoned"` - ValidationAttempts int `json:"validation_attempts"` - ValidationSuccesses int `json:"validation_successes"` - ValidationFailures int `json:"validation_failures"` - ValidationChecksPassed int `json:"validation_checks_passed"` - ValidationChecksFailed int `json:"validation_checks_failed"` + ValidationAttempts int `bson:"validation_attempts" json:"validation_attempts"` + ValidationSuccesses int `bson:"validation_successes" json:"validation_successes"` + ValidationFailures int `bson:"validation_failures" json:"validation_failures"` + ValidationChecksPassed int `bson:"validation_checks_passed" json:"validation_checks_passed"` + ValidationChecksFailed int `bson:"validation_checks_failed" json:"validation_checks_failed"` - LastTaskName string `json:"last_task_name"` - LastRunID int `json:"last_run_id"` + LastTaskName string `bson:"last_task_name" json:"last_task_name"` + LastRunID int `bson:"last_run_id" json:"last_run_id"` } type TaskMetricsFile struct { - TaskName string `json:"task_name"` - UpdatedAt time.Time `json:"updated_at"` - Summary TaskStatsSummary `json:"summary"` - Runs []TaskRunMetrics `json:"runs"` + ID primitive.ObjectID `bson:"_id" json:"id"` + ParticipantID primitive.ObjectID `bson:"participant_id" json:"participant_id"` + TaskName string `bson:"task_name" json:"task_name"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` + Summary TaskStatsSummary `bson:"summary" json:"summary"` + Runs []TaskRunMetrics `bson:"runs" json:"runs"` } type TaskStatsSummary struct { - TotalRuns int `json:"total_runs"` - CompletedRuns int `json:"completed_runs"` - IncompleteRuns int `json:"incomplete_runs"` - TotalDurationMs int64 `json:"total_duration_ms"` - AverageDurationMs int64 `json:"average_duration_ms"` - TotalUserActions int `json:"total_user_actions"` - QuestionnairesCompleted int `json:"questionnaires_completed"` - QuestionnairesAbandoned int `json:"questionnaires_abandoned"` - ValidationAttempts int `json:"validation_attempts"` - ValidationSuccesses int `json:"validation_successes"` - ValidationFailures int `json:"validation_failures"` - ValidationChecksPassed int `json:"validation_checks_passed"` - ValidationChecksFailed int `json:"validation_checks_failed"` - LastInterfaceType InterfaceType `json:"last_interface_type"` - FirstRunStartedAt time.Time `json:"first_run_started_at"` - LastRunStartedAt time.Time `json:"last_run_started_at"` - LastRunEndedAt time.Time `json:"last_run_ended_at"` + TotalRuns int `bson:"total_runs" json:"total_runs"` + CompletedRuns int `bson:"completed_runs" json:"completed_runs"` + IncompleteRuns int `bson:"incomplete_runs" json:"incomplete_runs"` + TotalDurationMs int64 `bson:"total_duration_ms" json:"total_duration_ms"` + AverageDurationMs int64 `bson:"average_duration_ms" json:"average_duration_ms"` + TotalUserActions int `bson:"total_user_actions" json:"total_user_actions"` + QuestionnairesCompleted int `bson:"questionnaires_completed" json:"questionnaires_completed"` + QuestionnairesAbandoned int `bson:"questionnaires_abandoned" json:"questionnaires_abandoned"` + ValidationAttempts int `bson:"validation_attempts" json:"validation_attempts"` + ValidationSuccesses int `bson:"validation_successes" json:"validation_successes"` + ValidationFailures int `bson:"validation_failures" json:"validation_failures"` + ValidationChecksPassed int `bson:"validation_checks_passed" json:"validation_checks_passed"` + ValidationChecksFailed int `bson:"validation_checks_failed" json:"validation_checks_failed"` + LastInterfaceType InterfaceType `bson:"last_interface_type" json:"last_interface_type"` + FirstRunStartedAt time.Time `bson:"first_run_started_at" json:"first_run_started_at"` + LastRunStartedAt time.Time `bson:"last_run_started_at" json:"last_run_started_at"` + LastRunEndedAt time.Time `bson:"last_run_ended_at" json:"last_run_ended_at"` } type TaskRunMetrics struct { - RunID int `json:"run_id"` - TaskName string `json:"task_name"` - InterfaceType InterfaceType `json:"interface_type"` - StartedAt time.Time `json:"started_at"` - EndedAt time.Time `json:"ended_at"` - DurationMs int64 `json:"duration_ms"` - Completed bool `json:"completed"` - ValidationAttempts int `json:"validation_attempts"` - ValidationSuccesses int `json:"validation_successes"` - ValidationFailures int `json:"validation_failures"` - ValidationChecksPassed int `json:"validation_checks_passed"` - ValidationChecksFailed int `json:"validation_checks_failed"` - LastValidationMessage string `json:"last_validation_message,omitempty"` - QuestionnaireCompleted bool `json:"questionnaire_completed"` - QuestionnaireUserQuit bool `json:"questionnaire_user_quit"` - QuestionnaireAnswers map[string]any `json:"questionnaire_answers,omitempty"` - Logs []TaskLogEntry `json:"logs"` - Error string `json:"error,omitempty"` + RunID int `bson:"run_id" json:"run_id"` + TaskName string `bson:"task_name" json:"task_name"` + InterfaceType InterfaceType `bson:"interface_type" json:"interface_type"` + StartedAt time.Time `bson:"started_at" json:"started_at"` + EndedAt time.Time `bson:"ended_at" json:"ended_at"` + DurationMs int64 `bson:"duration_ms" json:"duration_ms"` + Completed bool `bson:"completed" json:"completed"` + ValidationAttempts int `bson:"validation_attempts" json:"validation_attempts"` + ValidationSuccesses int `bson:"validation_successes" json:"validation_successes"` + ValidationFailures int `bson:"validation_failures" json:"validation_failures"` + ValidationChecksPassed int `bson:"validation_checks_passed" json:"validation_checks_passed"` + ValidationChecksFailed int `bson:"validation_checks_failed" json:"validation_checks_failed"` + LastValidationMessage string `bson:"last_validation_message,omitempty" json:"last_validation_message,omitempty"` + QuestionnaireCompleted bool `bson:"questionnaire_completed" json:"questionnaire_completed"` + QuestionnaireUserQuit bool `bson:"questionnaire_user_quit" json:"questionnaire_user_quit"` + QuestionnaireAnswers map[string]any `bson:"questionnaire_answers,omitempty" json:"questionnaire_answers,omitempty"` + Logs []TaskLogEntry `bson:"logs" json:"logs"` + Error string `bson:"error,omitempty" json:"error,omitempty"` } type TaskLogEntry struct { - Timestamp time.Time `json:"timestamp"` - Level string `json:"level"` - Message string `json:"message"` - Source string `json:"source,omitempty"` - Action string `json:"action,omitempty"` - Target string `json:"target,omitempty"` - Result string `json:"result,omitempty"` - Attempt int `json:"attempt,omitempty"` + Timestamp time.Time `bson:"timestamp" json:"timestamp"` + Level string `bson:"level" json:"level"` + Message string `bson:"message" json:"message"` + Source string `bson:"source,omitempty" json:"source,omitempty"` + Action string `bson:"action,omitempty" json:"action,omitempty"` + Target string `bson:"target,omitempty" json:"target,omitempty"` + Result string `bson:"result,omitempty" json:"result,omitempty"` + Attempt int `bson:"attempt,omitempty" json:"attempt,omitempty"` } diff --git a/internal/storage/mongo.go b/internal/storage/mongo.go new file mode 100644 index 0000000..f64d740 --- /dev/null +++ b/internal/storage/mongo.go @@ -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 +} diff --git a/pkg/task/lifecycle.go b/pkg/task/lifecycle.go index 8a9fa29..75b18d9 100644 --- a/pkg/task/lifecycle.go +++ b/pkg/task/lifecycle.go @@ -4,6 +4,7 @@ import ( "log/slog" "github.com/LazyBachelor/LazyPM/internal/models" + "go.mongodb.org/mongo-driver/bson/primitive" ) type RunLifecycle struct { @@ -23,9 +24,11 @@ func NewRunLifecycle(app *App, config Config, details models.TaskDetails, iType collector.recordUserAction(action) }) + var participantID primitive.ObjectID var store MetricsStore if config.StatisticsStoragePath != "" { - store = NewFileMetricsStore(config.StatisticsStoragePath, logger) + participantID = app.Stats.GetParticipantID() + store = NewFileMetricsStore(config.StatisticsStoragePath, participantID, logger) } return &RunLifecycle{ diff --git a/pkg/task/metrics_store.go b/pkg/task/metrics_store.go index ae69f25..a499de8 100644 --- a/pkg/task/metrics_store.go +++ b/pkg/task/metrics_store.go @@ -10,6 +10,7 @@ import ( "time" "github.com/LazyBachelor/LazyPM/internal/models" + "go.mongodb.org/mongo-driver/bson/primitive" ) type MetricsStore interface { @@ -17,14 +18,16 @@ type MetricsStore interface { } type FileMetricsStore struct { - path string - logger *slog.Logger + path string + 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{ - path: path, - logger: logger, + path: path, + participantID: participantID, + logger: logger, } } @@ -41,8 +44,10 @@ func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run mode } metrics := models.TaskMetricsFile{ - TaskName: taskName, - Runs: []models.TaskRunMetrics{}, + ID: primitive.NewObjectID(), + ParticipantID: s.participantID, + TaskName: taskName, + Runs: []models.TaskRunMetrics{}, } if err := readMetrics(&metrics, s.path); err != nil {