table())->where('id', $id)->first(); } /** * Open a new session. Returns the new session id. * $data must include visitor_id, entry_path, started_at, and any geo/UA/UTM fields. */ public function open(array $data): string { $id = $this->ids->generate(); $now = $data['started_at'] ?? Carbon::now()->toDateTimeString(); DB::table($this->table())->insert(array_merge($data, [ 'id' => $id, 'status' => 'open', 'page_count' => 0, 'is_bot' => $data['is_bot'] ?? 0, 'created_at' => $now, 'updated_at' => $now, ])); return $id; } /** * Called only by AnalyticsSessionTransitioner after validating the open→closed edge. */ public function close(string $id, string $exitPath, int $durationSeconds): void { $now = Carbon::now()->toDateTimeString(); DB::table($this->table())->where('id', $id)->update([ 'status' => 'closed', 'ended_at' => $now, 'exit_path' => $exitPath, 'duration_seconds' => $durationSeconds, 'updated_at' => $now, ]); } public function incrementPageCount(string $id): void { DB::table($this->table())->where('id', $id)->increment('page_count', 1, [ 'updated_at' => Carbon::now()->toDateTimeString(), ]); } public function updateExitPath(string $id, string $path): void { DB::table($this->table())->where('id', $id)->update([ 'exit_path' => $path, 'updated_at' => Carbon::now()->toDateTimeString(), ]); } /** * Open a session using the client-supplied UUID as the row id. * Idempotent: silently no-ops if that session_id already exists * (handles duplicate beacon delivery or concurrent requests). */ public function openIdempotent(string $sessionId, array $data): void { $exists = DB::table($this->table())->where('id', $sessionId)->exists(); if ($exists) { return; } $now = Carbon::now()->toDateTimeString(); DB::table($this->table())->insert(array_merge($data, [ 'id' => $sessionId, 'status' => 'open', 'page_count' => 0, 'is_bot' => 0, 'started_at' => $now, 'created_at' => $now, 'updated_at' => $now, ])); } }