create(array_merge([ 'title' => 'Analytics World ' . $slugSuffix, 'slug' => 'analytics-world-' . $slugSuffix, 'tagline' => 'Measured campaign storytelling.', 'summary' => 'A world used to verify analytics reporting.', 'description' => 'Analytics world description', 'theme_key' => 'summer', 'status' => World::STATUS_PUBLISHED, 'type' => World::TYPE_CAMPAIGN, 'is_featured' => true, 'published_at' => now()->subDays(5), 'starts_at' => now()->subDays(3), 'ends_at' => now()->addDays(10), 'created_by_user_id' => $creator->id, ], $attributes)); } it('records worlds analytics events through the public api', function (): void { $creator = User::factory()->create(); $world = analyticsWorld($creator); $this->postJson(route('api.worlds.analytics.events.store'), [ 'world_id' => $world->id, 'event_type' => 'world_cta_clicked', 'section_key' => 'hero', 'cta_key' => 'main_world_cta', 'entity_type' => 'world', 'entity_id' => $world->id, 'entity_title' => $world->title, 'source_surface' => 'homepage_spotlight', 'source_detail' => 'primary', 'visitor_token' => 'guest-analytics-token', ])->assertAccepted()->assertJson(['ok' => true]); $this->assertDatabaseHas('world_analytics_events', [ 'world_id' => $world->id, 'event_type' => 'world_cta_clicked', 'section_key' => 'hero', 'cta_key' => 'main_world_cta', 'entity_type' => 'world', 'entity_id' => $world->id, 'entity_title' => $world->title, 'source_surface' => 'homepage_spotlight', 'source_detail' => 'primary', 'viewer_type' => 'guest', 'visitor_key' => hash('sha256', 'visitor:guest-analytics-token'), ]); $this->postJson(route('api.worlds.analytics.events.store'), [ 'world_id' => $world->id, 'event_type' => 'world_source_impression', 'section_key' => 'spotlight', 'source_surface' => 'homepage_spotlight', 'source_detail' => 'primary', 'visitor_token' => 'guest-analytics-impression', ])->assertAccepted()->assertJson(['ok' => true]); $this->assertDatabaseHas('world_analytics_events', [ 'world_id' => $world->id, 'event_type' => 'world_source_impression', 'section_key' => 'spotlight', 'source_surface' => 'homepage_spotlight', 'source_detail' => 'primary', 'viewer_type' => 'guest', 'visitor_key' => hash('sha256', 'visitor:guest-analytics-impression'), ]); }); it('includes analytics summaries and edition comparison on studio world pages', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'analyticsmod-' . Str::lower(Str::random(6)), ]); $groupOwner = User::factory()->create([ 'username' => 'challenge-owner-' . Str::lower(Str::random(6)), ]); $group = Group::factory()->create([ 'name' => 'Retro Group ' . Str::upper(Str::random(4)), 'slug' => 'retro-group-' . Str::lower(Str::random(4)), ]); $challenge = GroupChallenge::query()->create([ 'group_id' => $group->id, 'title' => 'Retro Challenge ' . Str::upper(Str::random(4)), 'slug' => 'retro-challenge-' . Str::lower(Str::random(4)), 'summary' => 'A linked challenge for analytics verification.', 'description' => 'Challenge description', 'visibility' => GroupChallenge::VISIBILITY_PUBLIC, 'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC, 'status' => GroupChallenge::STATUS_ACTIVE, 'start_at' => now()->subDay(), 'end_at' => now()->addDays(7), 'created_by_user_id' => $groupOwner->id, ]); $currentWorld = analyticsWorld($moderator, [ 'title' => 'Retro Month 2026', 'slug' => 'retro-month-2026-' . Str::lower(Str::random(4)), 'recurrence_key' => 'retro-month', 'edition_year' => 2026, 'linked_challenge_id' => $challenge->id, ]); $previousWorld = analyticsWorld($moderator, [ 'title' => 'Retro Month 2025', 'slug' => 'retro-month-2025-' . Str::lower(Str::random(4)), 'recurrence_key' => 'retro-month', 'edition_year' => 2025, 'starts_at' => now()->subYear(), 'ends_at' => now()->subYear()->addDays(10), 'published_at' => now()->subYear()->subDays(2), ]); $artwork = Artwork::factory()->for($moderator)->create([ 'title' => 'Analytics Artwork', 'slug' => 'analytics-artwork-' . Str::lower(Str::random(4)), 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); WorldSubmission::query()->create([ 'world_id' => $currentWorld->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $moderator->id, 'status' => WorldSubmission::STATUS_LIVE, 'is_featured' => true, 'created_at' => now()->subDay(), 'updated_at' => now()->subDay(), ]); WorldRewardGrant::query()->create([ 'user_id' => $moderator->id, 'world_id' => $currentWorld->id, 'artwork_id' => $artwork->id, 'reward_type' => 'winner', 'grant_source' => 'manual', 'granted_at' => now()->subHours(6), ]); WorldRewardGrant::query()->create([ 'user_id' => $moderator->id, 'world_id' => $previousWorld->id, 'artwork_id' => $artwork->id, 'reward_type' => 'featured', 'grant_source' => 'manual', 'granted_at' => now()->subYear(), ]); collect([ [ 'world_id' => $currentWorld->id, 'event_type' => 'world_source_impression', 'section_key' => 'spotlight', 'source_surface' => 'homepage_spotlight', 'source_detail' => 'primary', 'visitor_key' => hash('sha256', 'visitor:impression-one'), 'occurred_at' => Carbon::now()->subHours(4), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_source_impression', 'section_key' => 'card', 'source_surface' => 'worlds_index', 'source_detail' => 'featured', 'visitor_key' => hash('sha256', 'visitor:impression-two'), 'occurred_at' => Carbon::now()->subHours(3), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_viewed', 'source_surface' => 'homepage_spotlight', 'source_detail' => 'primary', 'visitor_key' => hash('sha256', 'visitor:viewer-one'), 'occurred_at' => Carbon::now()->subHours(4), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_viewed', 'source_surface' => 'worlds_index', 'source_detail' => 'featured', 'visitor_key' => hash('sha256', 'visitor:viewer-two'), 'occurred_at' => Carbon::now()->subHours(3), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_source_clicked', 'source_surface' => 'homepage_spotlight', 'source_detail' => 'primary', 'entity_type' => 'world', 'entity_id' => $currentWorld->id, 'entity_title' => $currentWorld->title, 'visitor_key' => hash('sha256', 'visitor:viewer-one'), 'occurred_at' => Carbon::now()->subHours(4), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_cta_clicked', 'section_key' => 'hero', 'cta_key' => 'main_world_cta', 'source_surface' => 'homepage_spotlight', 'visitor_key' => hash('sha256', 'visitor:viewer-one'), 'occurred_at' => Carbon::now()->subHours(4), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_challenge_cta_clicked', 'section_key' => 'challenge', 'challenge_id' => $challenge->id, 'visitor_key' => hash('sha256', 'visitor:challenge-viewer'), 'occurred_at' => Carbon::now()->subHours(2), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_submission_created', 'section_key' => 'community_submissions', 'source_surface' => 'upload_flow', 'entity_type' => 'artwork', 'entity_id' => $artwork->id, 'entity_title' => $artwork->title, 'visitor_key' => hash('sha256', 'system:submission'), 'occurred_at' => Carbon::now()->subHours(2), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_submission_approved', 'section_key' => 'community_submissions', 'source_surface' => 'upload_flow', 'entity_type' => 'artwork', 'entity_id' => $artwork->id, 'entity_title' => $artwork->title, 'visitor_key' => hash('sha256', 'system:approval'), 'occurred_at' => Carbon::now()->subHours(2), ], [ 'world_id' => $currentWorld->id, 'event_type' => 'world_reward_granted', 'section_key' => 'rewards', 'entity_type' => 'artwork', 'entity_id' => $artwork->id, 'entity_title' => $artwork->title, 'visitor_key' => hash('sha256', 'system:reward'), 'occurred_at' => Carbon::now()->subHours(1), ], [ 'world_id' => $previousWorld->id, 'event_type' => 'world_viewed', 'source_surface' => 'navigation', 'source_detail' => 'archive', 'visitor_key' => hash('sha256', 'visitor:archive-viewer'), 'occurred_at' => Carbon::now()->subYear(), ], ])->each(function (array $attributes) use ($currentWorld, $previousWorld): void { $world = (int) $attributes['world_id'] === (int) $currentWorld->id ? $currentWorld : $previousWorld; WorldAnalyticsEvent::query()->create(array_merge([ 'world_slug' => $world->slug, 'world_type' => $world->type, 'recurrence_key' => $world->recurrence_key, 'edition_year' => $world->edition_year, 'viewer_type' => 'guest', 'user_id' => null, 'meta' => null, ], $attributes)); }); $this->actingAs($moderator) ->get(route('studio.worlds.edit', ['world' => $currentWorld->id])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioWorldEditor') ->where('world.analytics.ranges.30d.summary.views', 2) ->where('world.analytics.ranges.30d.summary.unique_visitors', 2) ->where('world.analytics.ranges.30d.summary.promotion_impressions', 2) ->where('world.analytics.ranges.30d.summary.cta_clicks', 1) ->where('world.analytics.ranges.30d.summary.reward_grants', 1) ->where('world.analytics.ranges.30d.participation.live', 1) ->where('world.analytics.ranges.30d.sources.0.impressions', 1) ->where('world.analytics.ranges.30d.sources.0.clickthrough_rate', 1) ->where('world.analytics.ranges.30d.challenge.linked_challenge_id', $challenge->id) ->where('world.analytics.ranges.30d.challenge.click_to_submission_conversion', 1) ->where('world.analytics.ranges.30d.sources.0.source_surface', 'homepage_spotlight') ->where('world.analytics.edition_comparison.recurrence_key', 'retro-month') ->has('world.analytics.edition_comparison.editions', 2)); });