create(['role' => 'admin']); $user = User::factory()->create(); $artwork = Artwork::factory()->create(); $previousDate = now()->subDay()->toDateString(); $date = now()->toDateString(); $recordEvent = function (string $eventDate, string $eventType, array $meta = []) use ($user, $artwork) { $algoVersion = (string) ($meta['algo_version'] ?? 'clip-cosine-v2-adaptive'); unset($meta['algo_version']); DB::table('user_discovery_events')->insert([ 'event_id' => (string) Str::uuid(), 'user_id' => $user->id, 'artwork_id' => $artwork->id, 'category_id' => null, 'event_type' => $eventType, 'event_version' => 'event-v1', 'algo_version' => $algoVersion, 'weight' => 1.0, 'event_date' => $eventDate, 'occurred_at' => now(), 'meta' => json_encode(array_merge([ 'gallery_type' => 'for-you', 'surface' => 'for-you', ], $meta), JSON_THROW_ON_ERROR), 'created_at' => now(), 'updated_at' => now(), ]); }; $recordEvent($previousDate, 'view'); $recordEvent($previousDate, 'click'); $recordEvent($previousDate, 'favorite'); $recordEvent($previousDate, 'hide_artwork', ['reason' => 'not_relevant']); $recordEvent($previousDate, 'dislike_tag', ['tag_slug' => 'abstract']); $recordEvent($previousDate, 'view', ['gallery_type' => 'homepage', 'surface' => 'homepage', 'algo_version' => 'clip-cosine-v1']); $recordEvent($previousDate, 'click', ['gallery_type' => 'homepage', 'surface' => 'homepage', 'algo_version' => 'clip-cosine-v1']); $recordEvent($previousDate, 'favorite', ['gallery_type' => 'homepage', 'surface' => 'homepage', 'algo_version' => 'clip-cosine-v1']); $recordEvent($date, 'view'); $recordEvent($date, 'click'); $recordEvent($date, 'favorite'); $recordEvent($date, 'download'); $recordEvent($date, 'hide_artwork', ['reason' => 'not_relevant']); $recordEvent($date, 'unhide_artwork', ['reason' => 'undo']); $recordEvent($date, 'view', ['gallery_type' => 'homepage', 'surface' => 'homepage', 'algo_version' => 'clip-cosine-v1']); $recordEvent($date, 'click', ['gallery_type' => 'homepage', 'surface' => 'homepage', 'algo_version' => 'clip-cosine-v1']); $recordEvent($date, 'hide_artwork', ['gallery_type' => 'homepage', 'surface' => 'homepage', 'algo_version' => 'clip-cosine-v1', 'reason' => 'not_relevant']); $recordEvent($date, 'dislike_tag', ['gallery_type' => 'homepage', 'surface' => 'homepage', 'algo_version' => 'clip-cosine-v1', 'tag_slug' => 'portrait']); $this->artisan('analytics:aggregate-discovery-feedback', ['--date' => $previousDate]) ->assertExitCode(0); $this->artisan('analytics:aggregate-discovery-feedback', ['--date' => $date]) ->assertExitCode(0); $response = $this->actingAs($admin) ->getJson('/api/admin/reports/discovery-feedback?from=' . $previousDate . '&to=' . $date . '&limit=10'); $response->assertOk(); $response->assertJsonPath('overview.views', 4); $response->assertJsonPath('overview.clicks', 4); $response->assertJsonPath('overview.feedback_actions', 4); $response->assertJsonPath('overview.hidden_artworks', 3); $response->assertJsonPath('overview.disliked_tags', 2); $response->assertJsonPath('overview.negative_feedback_actions', 5); $response->assertJsonPath('overview.undo_hidden_artworks', 1); $response->assertJsonPath('overview.undo_disliked_tags', 0); $response->assertJsonPath('overview.undo_actions', 1); $response->assertJsonPath('daily_feedback.0.negative_feedback_actions', 2); $response->assertJsonPath('daily_feedback.1.negative_feedback_actions', 3); $response->assertJsonPath('daily_feedback.1.undo_actions', 1); $response->assertJsonPath('trend_summary.latest_day.date', $date); $response->assertJsonPath('trend_summary.previous_day.date', $previousDate); $response->assertJsonPath('trend_summary.rolling_7d_average.feedback_actions', 2); $response->assertJsonPath('trend_summary.rolling_7d_average.negative_feedback_actions', 2.5); $response->assertJsonPath('trend_summary.rolling_7d_average.undo_actions', 0.5); $response->assertJsonPath('trend_summary.deltas.feedback_actions.label', 'Flat'); $response->assertJsonPath('trend_summary.deltas.negative_feedback_actions.label', 'Worse +1 vs prev day'); $response->assertJsonPath('trend_summary.overall_status.level', 'watch'); $response->assertJsonPath('by_surface.0.surface', 'homepage'); $response->assertJsonPath('by_surface.0.negative_feedback_actions', 2); $response->assertJsonPath('by_surface.0.trend.overall_status.level', 'risk'); $response->assertJsonPath('by_surface.0.trend.deltas.feedback_actions.label', 'Worse -1 vs prev day'); $response->assertJsonPath('by_surface.0.trend.deltas.negative_feedback_actions.label', 'Worse +2 vs prev day'); $response->assertJsonPath('by_surface.1.surface', 'for-you'); $response->assertJsonPath('by_surface.1.trend.overall_status.level', 'healthy'); $response->assertJsonPath('by_algo_surface.0.algo_version', 'clip-cosine-v1'); $response->assertJsonPath('by_algo_surface.0.surface', 'homepage'); $response->assertJsonPath('by_algo_surface.0.negative_feedback_actions', 2); $response->assertJsonPath('by_algo_surface.0.trend.overall_status.level', 'risk'); $response->assertJsonPath('by_algo_surface.0.trend.deltas.feedback_actions.label', 'Worse -1 vs prev day'); $response->assertJsonPath('by_algo_surface.0.trend.deltas.negative_feedback_actions.label', 'Worse +2 vs prev day'); $response->assertJsonPath('by_algo_surface.1.algo_version', 'clip-cosine-v2-adaptive'); $response->assertJsonPath('top_artworks.0.artwork_id', $artwork->id); $response->assertJsonPath('top_artworks.0.negative_feedback_actions', 3); $response->assertJsonPath('top_artworks.0.undo_actions', 1); $response->assertJsonPath('latest_aggregated_date', $date); });