website = Website::factory()->create(); } private function makeService(?Carbon $from = null, ?Carbon $to = null): AnalyticsQueryService { return AnalyticsQueryService::for( $this->website, $from ?? Carbon::today()->subDays(6), $to ?? Carbon::today(), ); } private function createVisitorWithPageViews(int $pvCount, ?Carbon $viewedAt = null): void { $visitor = Visitor::factory()->for($this->website)->create(); $session = AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor->id, 'started_at' => $viewedAt ?? now(), 'ended_at' => $viewedAt ?? now(), 'duration' => 60, 'pages_count' => $pvCount, 'is_bounce' => $pvCount <= 1, ]); for ($i = 0; $i < $pvCount; $i++) { PageView::factory()->for($this->website)->create([ 'visitor_id' => $visitor->id, 'analytics_session_id' => $session->id, 'viewed_at' => $viewedAt ?? now(), 'path' => '/page-'.$i, 'time_on_page' => 30, ]); } } // -- Summary metrics from raw data -- public function test_total_page_views_counts_correctly(): void { $this->createVisitorWithPageViews(3); $this->createVisitorWithPageViews(2); $service = $this->makeService(); $this->assertEquals(5, $service->totalPageViews()); } public function test_unique_visitors_counts_distinct(): void { $this->createVisitorWithPageViews(2); $this->createVisitorWithPageViews(1); $service = $this->makeService(); $this->assertEquals(2, $service->uniqueVisitors()); } public function test_total_sessions(): void { $this->createVisitorWithPageViews(2); $this->createVisitorWithPageViews(1); $service = $this->makeService(); $this->assertEquals(2, $service->totalSessions()); } public function test_bounce_rate_calculation(): void { // Non-bounce session $this->createVisitorWithPageViews(3); // Bounce session $visitor = Visitor::factory()->for($this->website)->create(); AnalyticsSession::factory()->for($this->website)->bounce()->create([ 'visitor_id' => $visitor->id, 'started_at' => now(), ]); $service = $this->makeService(); $this->assertEquals(50.0, $service->bounceRate()); } public function test_bounce_rate_zero_when_no_sessions(): void { $service = $this->makeService(); $this->assertEquals(0, $service->bounceRate()); } public function test_avg_session_duration(): void { $visitor1 = Visitor::factory()->for($this->website)->create(); AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor1->id, 'duration' => 100, 'started_at' => now(), ]); $visitor2 = Visitor::factory()->for($this->website)->create(); AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor2->id, 'duration' => 200, 'started_at' => now(), ]); $service = $this->makeService(); $this->assertEquals(150, $service->avgSessionDuration()); } public function test_summary_returns_all_keys(): void { $service = $this->makeService(); $summary = $service->summary(); $this->assertArrayHasKey('page_views', $summary); $this->assertArrayHasKey('visitors', $summary); $this->assertArrayHasKey('sessions', $summary); $this->assertArrayHasKey('bounce_rate', $summary); $this->assertArrayHasKey('avg_duration', $summary); $this->assertArrayHasKey('avg_pages', $summary); } // -- Date range filtering -- public function test_only_counts_data_within_date_range(): void { $this->createVisitorWithPageViews(2, Carbon::today()); $this->createVisitorWithPageViews(1, Carbon::today()->subDays(30)); $service = $this->makeService(Carbon::today(), Carbon::today()); $this->assertEquals(2, $service->totalPageViews()); $this->assertEquals(1, $service->uniqueVisitors()); } // -- Aggregate data -- public function test_reads_from_aggregate_tables_for_past_days(): void { DailyAggregate::create([ 'website_id' => $this->website->id, 'date' => Carbon::yesterday()->toDateString(), 'visitors' => 50, 'sessions' => 80, 'page_views' => 200, 'bounce_count' => 20, 'total_duration' => 4000, ]); $service = $this->makeService( Carbon::yesterday(), Carbon::yesterday(), ); $this->assertEquals(200, $service->totalPageViews()); $this->assertEquals(50, $service->uniqueVisitors()); $this->assertEquals(80, $service->totalSessions()); } // -- Top pages -- public function test_top_pages_returns_ordered_results(): void { $visitor = Visitor::factory()->for($this->website)->create(); $session = AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor->id, 'started_at' => now(), ]); PageView::factory()->for($this->website)->count(3)->create([ 'visitor_id' => $visitor->id, 'analytics_session_id' => $session->id, 'path' => '/popular', 'viewed_at' => now(), ]); PageView::factory()->for($this->website)->create([ 'visitor_id' => $visitor->id, 'analytics_session_id' => $session->id, 'path' => '/less-popular', 'viewed_at' => now(), ]); $service = $this->makeService(); $pages = $service->topPages(10); $this->assertGreaterThanOrEqual(2, $pages->count()); $this->assertEquals('/popular', $pages->first()->path); } // -- Visitors over time -- public function test_visitors_over_time_returns_collection(): void { $this->createVisitorWithPageViews(1); $service = $this->makeService(); $timeseries = $service->visitorsOverTime(); $this->assertNotEmpty($timeseries); $first = $timeseries->first(); $this->assertArrayHasKey('period', $first); $this->assertArrayHasKey('visitors', $first); $this->assertArrayHasKey('page_views', $first); $this->assertArrayHasKey('sessions', $first); } // -- Traffic sources -- public function test_traffic_sources_groups_by_referrer(): void { $visitor = Visitor::factory()->for($this->website)->create(); AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor->id, 'referrer_domain' => 'google.com', 'started_at' => now(), ]); AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor->id, 'referrer_domain' => '', 'started_at' => now(), ]); $service = $this->makeService(); $sources = $service->trafficSources(); $this->assertGreaterThanOrEqual(1, $sources->count()); } // -- Segment filtering -- public function test_segment_filters_data_by_visitor_ids(): void { $visitor1 = Visitor::factory()->for($this->website)->create(['country' => 'DE']); $visitor2 = Visitor::factory()->for($this->website)->create(['country' => 'US']); $session1 = AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor1->id, 'started_at' => now(), ]); $session2 = AnalyticsSession::factory()->for($this->website)->create([ 'visitor_id' => $visitor2->id, 'started_at' => now(), ]); PageView::factory()->for($this->website)->count(3)->create([ 'visitor_id' => $visitor1->id, 'analytics_session_id' => $session1->id, 'viewed_at' => now(), ]); PageView::factory()->for($this->website)->count(2)->create([ 'visitor_id' => $visitor2->id, 'analytics_session_id' => $session2->id, 'viewed_at' => now(), ]); $service = $this->makeService(); $service->withSegment(collect([$visitor1->id])); $this->assertEquals(3, $service->totalPageViews()); $this->assertEquals(1, $service->uniqueVisitors()); $this->assertEquals(1, $service->totalSessions()); } }