diff --git a/app/Helper/MWTimestampHelper.php b/app/Helper/MWTimestampHelper.php index a5d03f71..6ce9f0ee 100644 --- a/app/Helper/MWTimestampHelper.php +++ b/app/Helper/MWTimestampHelper.php @@ -9,16 +9,12 @@ namespace App\Helper; use Carbon\CarbonImmutable; -use Carbon\Exceptions\InvalidFormatException; class MWTimestampHelper { private const MWTimestampFormat = 'YmdHis'; public static function getCarbonFromMWTimestamp(string $MWTimestamp): CarbonImmutable { $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); - if ($carbon === null) { - throw new InvalidFormatException('Unable to create Carbon object'); - } return $carbon; } diff --git a/app/Http/Controllers/ConversionMetricController.php b/app/Http/Controllers/ConversionMetricController.php index 2074fa20..ab66f52d 100644 --- a/app/Http/Controllers/ConversionMetricController.php +++ b/app/Http/Controllers/ConversionMetricController.php @@ -34,13 +34,16 @@ public function index(Request $request) { $daysSinceLastEdit = null; if ($wikiLastEditedTime !== null) { + // cast to int to retain Carbon 2 behaviour of using whole days $daysSinceLastEdit = (int) $wikiLastEditedTime->diffInDays($current_date, false); } if ($daysSinceLastEdit !== null && $daysSinceLastEdit >= 90) { + // cast to int to retain Carbon 2 behaviour of using whole days $time_before_wiki_abandoned_days = (int) $wiki->created_at->diffInDays($wikiLastEditedTime, false); } if ($wikiFirstEditedTime !== null) { + // cast to int to retain Carbon 2 behaviour of using whole days $time_to_engage_days = (int) $wiki->created_at->diffInDays($wikiFirstEditedTime, false); } $wiki_number_of_editors = $wiki->wikiSiteStats()->first()['activeusers'] ?? null; diff --git a/app/Jobs/PlatformStatsSummaryJob.php b/app/Jobs/PlatformStatsSummaryJob.php index 7bede260..a8c5beeb 100644 --- a/app/Jobs/PlatformStatsSummaryJob.php +++ b/app/Jobs/PlatformStatsSummaryJob.php @@ -132,6 +132,8 @@ public function prepareStats(array $allStats, $wikis): array { // is it edited in the last 90 days? if (!is_null($stats['lastEdit'])) { $lastTimestamp = MWTimestampHelper::getCarbonFromMWTimestamp(intval($stats['lastEdit'])); + // cast to int retain Carbon 2 behaviour of using whole days + // pass `$absolute = true` argument to retain Carbon 2 behaviour of returning an absolute diff $diff = (int) $lastTimestamp->diffInSeconds($currentTime, true); if ($diff <= $this->inactiveThreshold) { diff --git a/app/Jobs/SendEmptyWikiNotificationsJob.php b/app/Jobs/SendEmptyWikiNotificationsJob.php index 76a4e5d7..4e56eee7 100644 --- a/app/Jobs/SendEmptyWikiNotificationsJob.php +++ b/app/Jobs/SendEmptyWikiNotificationsJob.php @@ -34,6 +34,7 @@ public function checkIfWikiIsOldAndEmpty(Wiki $wiki) { $emptyDaysThreshold = config('wbstack.wiki_empty_notification_threshold'); $createdAt = $wiki->created_at; $now = CarbonImmutable::now(); + // cast to int to retain Carbon 2 behaviour of using whole days $emptyWikiDays = (int) $createdAt->diffInDays($now, true); $firstEdited = $wiki->wikiLifecycleEvents->first_edited; diff --git a/tests/Jobs/PlatformStatsSummaryJobTest.php b/tests/Jobs/PlatformStatsSummaryJobTest.php index 4a954200..6a00c74b 100644 --- a/tests/Jobs/PlatformStatsSummaryJobTest.php +++ b/tests/Jobs/PlatformStatsSummaryJobTest.php @@ -274,4 +274,55 @@ public function testCreationStats() { ); } + + public function testPrepareStatsTreatsSecondPrecisionTimestampAtThresholdAsActive() { + $currentTime = CarbonImmutable::now(); + + $wiki = Wiki::factory()->create(['deleted_at' => null, 'domain' => 'thresholdtest.com']); + WikiDb::create([ + 'name' => 'mwdb_threshold_' . $wiki->id, + 'user' => 'user', + 'password' => 'password', + 'version' => 'version', + 'prefix' => 'prefix', + 'wiki_id' => $wiki->id, + ]); + + Http::fake([ + $this->mwBackendHost . '/w/api.php?action=query&list=allpages&apnamespace=122&apcontinue=&aplimit=max&format=json' => Http::response([ + 'query' => ['allpages' => []], + ], 200), + $this->mwBackendHost . '/w/api.php?action=query&list=allpages&apnamespace=120&apcontinue=&aplimit=max&format=json' => Http::response([ + 'query' => ['allpages' => []], + ], 200), + ]); + + $job = new PlatformStatsSummaryJob; + + // This is a hack to override the `private` `PlatformStatsSummaryJob::mwHostResolver` property. + // See https://www.php.net/manual/en/closure.call.php for more details on how this works. + // TODO: figure out how to stub the `DatabaseManager` correctly and/or refactor the Job so that + // we can more easily inject dependencies in the tests. + (function ($resolver): void { + $this->mwHostResolver = $resolver; + })->call($job, $this->mockMwHostResolver); + + $groups = $job->prepareStats([ + [ + 'wiki' => 'thresholdtest.com', + 'edits' => 1, + 'pages' => 1, + 'users' => 1, + 'active_users' => 1, + 'lastEdit' => MWTimestampHelper::getMWTimestampFromCarbon( + $currentTime->subSeconds(config('wbstack.platform_summary_inactive_threshold')) + ), + 'first100UsingOauth' => '0', + 'platform_summary_version' => 'v1', + ], + ], [$wiki]); + + $this->assertSame(1, $groups['edited_last_90_days']); + $this->assertSame(0, $groups['not_edited_last_90_days']); + } } diff --git a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php index 7fb09ba3..7a2ddf21 100644 --- a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php +++ b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php @@ -38,7 +38,7 @@ public function testEmptyWikiNotificationsSendNotification() { Notification::fake(); $user = User::factory()->create(['verified' => true]); $wiki = Wiki::factory()->create(['created_at' => $thresholdDaysAgo]); - $manager = WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); + WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); $wiki->wikiLifecycleEvents()->updateOrCreate(['first_edited' => null]); $job = new SendEmptyWikiNotificationsJob; @@ -50,6 +50,25 @@ public function testEmptyWikiNotificationsSendNotification() { ); } + // empty wikis, that are almost old enough (29 days and 23 hrs) + public function testEmptyWikiNotificationsNotificationNotSent() { + $thresholdDaysAgo = Carbon::now() + ->subDays((config('wbstack.wiki_empty_notification_threshold') - 1)) + ->subHours(23) + ->toDateTimeString(); + + Notification::fake(); + $user = User::factory()->create(['verified' => true]); + $wiki = Wiki::factory()->create(['created_at' => $thresholdDaysAgo]); + WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); + $wiki->wikiLifecycleEvents()->updateOrCreate(['first_edited' => null]); + + $job = new SendEmptyWikiNotificationsJob; + $job->handle(); + + Notification::assertNothingSent(); + } + // fresh wiki that does not have lifecycle event records yet public function testEmptyWikiNotificationsFreshWiki() { $now = Carbon::now()->toDateTimeString(); diff --git a/tests/Routes/Wiki/ConversionMetricTest.php b/tests/Routes/Wiki/ConversionMetricTest.php index a1c47940..24e06ce3 100644 --- a/tests/Routes/Wiki/ConversionMetricTest.php +++ b/tests/Routes/Wiki/ConversionMetricTest.php @@ -134,6 +134,39 @@ public function testDownloadJson() { ); } + public function testDownloadJsonTruncatesFractionalDayDiffs() { + $currentDate = CarbonImmutable::now(); + $createdAt = $currentDate->subDays(200)->subHours(12); // 200.5 days ago + $firstEditedAt = $createdAt->addDays(1)->addHours(12); // 1.5 days after + $lastEditedAt = $currentDate->subDays(100); // 100 days ago + + $wiki = Wiki::factory()->create([ + 'domain' => 'fractional.days.cloud', + 'sitename' => 'Fractional Days Site', + ]); + WikiSiteStats::factory()->create([ + 'wiki_id' => $wiki->id, + 'pages' => 77, + 'activeusers' => 2, + ]); + $wiki->created_at = $createdAt; + $wiki->wikiLifecycleEvents()->updateOrCreate([ + 'first_edited' => $firstEditedAt, + 'last_edited' => $lastEditedAt, + ]); + $wiki->save(); + + $response = $this->getJson($this->route); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'domain' => 'fractional.days.cloud', + 'time_to_engage_days' => 1, + 'time_before_wiki_abandoned_days' => 100, + 'number_of_active_editors' => 2, + ]); + } + public function testFunctionalWithMissingLifecycleEventsandStats() { $wiki = Wiki::factory()->create([ 'domain' => 'very.new.wikibase.cloud', 'sitename' => 'bsite',