app/classes/ReleaseInsights/Beta.php (199 lines of code) (raw):

<?php declare(strict_types=1); namespace ReleaseInsights; use GuzzleHttp\Client; use GuzzleHttp\Promise\Utils as Promise; use GuzzleHttp\HandlerStack; use Kevinrob\GuzzleCache\CacheMiddleware; use Kevinrob\GuzzleCache\Storage\Psr6CacheStorage; use Kevinrob\GuzzleCache\Strategy\GreedyCacheStrategy; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use ReleaseInsights\Bugzilla; use ReleaseInsights\Release; use ReleaseInsights\Json; use ReleaseInsights\URL; readonly class Beta { public int $count; public int $number_betas; public bool $beta_cycle_ended; public function __construct(public int $release = BETA) { $this->count = (int) explode('b', FIREFOX_BETA)[1]; // We get the number of betas from the planned schedule $schedule = new Release((string) $release)->getSchedule(); $schedule = array_keys($schedule); $schedule = array_filter($schedule, fn($label) => str_starts_with($label, 'beta_')); $this->number_betas = count($schedule); // Check if the beta cycle is over, this avoids a miscount for RC builds if ($this->count >= $this->number_betas && ! defined('TESTING_CONTEXT')) { // @codeCoverageIgnoreStart // TODO: cache this request as it can take multiple seconds when hg is slow $this->beta_cycle_ended = str_contains((string) get_headers( URL::Mercurial->value . 'releases/mozilla-beta/json-pushes?fromchange=' . 'FIREFOX_BETA_' . BETA . '_END')[0], '200'); // @codeCoverageIgnoreEnd } else { $this->beta_cycle_ended = false; } } /** * @return array<mixed> */ public function getLogEndpoints(): array { $hg_end_points = []; [$have_rc, $number_rc_builds] = $this->RCStatus(); /* Analyse Beta logs first */ foreach (range(0, $this->count) as $beta_number) { $beta_start = ($beta_number == 0) ? 'FIREFOX_BETA_' . BETA . '_BASE' : 'FIREFOX_' . BETA . '_0b' . $beta_number . '_RELEASE'; $beta_end = 'FIREFOX_' . BETA . '_0b' . (string) ($beta_number + 1) . '_RELEASE'; if ($beta_number == $this->count) { $beta_end = 'tip'; // Just after merge day, we don't want to use tip for beta_end but the newly created tag if ($this->beta_cycle_ended) { $beta_end = 'FIREFOX_BETA_' . BETA . '_END'; // @codeCoverageIgnore } } $beta_version = (string) BETA . '.0b' . (string) ($beta_number + 1); // This is what landed on mozilla-beta after the last beta but before the merge and RC1 $beta_version = ($beta_number == $this->number_betas) ? (string) BETA . '.0rc0' // @codeCoverageIgnore : (string) BETA . '.0b' . (string) ($beta_number + 1); $hg_end_points[$beta_version] = 'releases/mozilla-beta/json-pushes?fromchange=' . $beta_start . '&amp;tochange=' . $beta_end . '&amp;full&amp;version=2'; } /* Analyse Release logs for RCs if we are in RC week Check if we have already shipped a Release Candidate build to the beta channel Remote balrog API can give a 404, we have a fallback to N/A */ if ($have_rc) { foreach (range(1, $number_rc_builds) as $rc_number) { if ($rc_number == 1) { $rc_start = 'FIREFOX_RELEASE_' . BETA . '_BASE'; $rc_end = 'FIREFOX_' . BETA . '_0_BUILD1'; } else { // @codeCoverageIgnoreStart $rc_start = 'FIREFOX_' . BETA . '_0_BUILD' . (string) ($rc_number - 1); $rc_end = 'FIREFOX_' . BETA . '_0_BUILD' . (string) ($rc_number); // @codeCoverageIgnoreEnd } $rc_version = (string) BETA . '.0rc' . (string) $rc_number; // This is what landed on mozilla-beta after the last beta but before the merge and RC1 $hg_end_points[$rc_version] = 'releases/mozilla-release/json-pushes?fromchange=' . $rc_start . '&amp;tochange=' . $rc_end . '&amp;full&amp;version=2'; } } return $hg_end_points; } /** * We don't unit test this function as this is all http requests * * @return array<mixed> * @codeCoverageIgnore */ public function getBugsFromLogs(): array { // Create a HandlerStack $stack = HandlerStack::create(); $TTL = 600; $cache_storage = new Psr6CacheStorage( new FilesystemAdapter( 'guzzle', // Cache folder name $TTL, CACHE_PATH ) ); // Add Cache Method $stack->push( new CacheMiddleware( new GreedyCacheStrategy( $cache_storage, $TTL // the TTL in seconds ) ), 'greedy-cache' ); // Initialize the client with the handler option $client = new Client(['handler' => $stack, 'base_uri' => URL::Mercurial->value]); // Initiate each request but do not block $promises = []; foreach ($this->getLogEndpoints() as $beta => $query) { $promises[$beta] = $client->getAsync($query); } $responses = Promise::settle($promises)->wait(); $beta_logs = []; foreach ($responses as $key => $json_log) { $data = $json_log['value']->getBody()->getContents(); $beta_logs[$key] = Bugzilla::getBugsFromHgWeb( query: $data, detect_backouts: true, cache_ttl: 3600*24 ); } // Wait for the requests to complete, even if some of them fail return $beta_logs; } /** * Function relies heavily on external data, hard to unit test * @return array<mixed> * @codeCoverageIgnore */ public function uplifts(): array { $uplifts_per_beta = $this->getBugsFromLogs(); $log_links = array_map(fn($query) => URL::Mercurial->value . $query, $this->getLogEndpoints()); $log_links = array_map(fn($query) => str_replace('json-pushes', 'pushloghtml', $query), $log_links); foreach ($log_links as $beta => $url) { $uplifts_per_beta[$beta]['hg_link'] = $url; } foreach ($log_links as $beta => $url) { $uplifts_per_beta[$beta]['bugzilla'] = Bugzilla::getBugListLink($uplifts_per_beta[$beta]['total']); } // We use a natural sort to avoid having a beta 10 listed after beta 1 ksort($uplifts_per_beta, SORT_NATURAL); return $uplifts_per_beta; } /** * Return all beta crashes * @return array<mixed> */ public function crashes(): array { $data = []; foreach (range(1, $this->count) as $beta_number) { $beta_number = (string) $this->release . '.0b' . (string) $beta_number; if (defined('TESTING_CONTEXT')) { $beta_number = str_replace('94', '131', $beta_number); $target = URL::Socorro->target() . 'crash-stats.mozilla.org_' . $beta_number . '.json'; } else { $target = URL::Socorro->value . 'SuperSearch/?version=' . $beta_number . '&_facets=signature&product=Firefox'; // @codeCoverageIgnore } $temp = Json::load($target, 3600); // In local tests, we always have test data // @codeCoverageIgnoreStart if (empty($temp)) { $data[$beta_number] = [ 'total' => 0, 'signatures' => [], ]; continue; } // @codeCoverageIgnoreEnd $data[$beta_number] = [ 'total' => $temp['total'], 'signatures' => $temp['facets']['signature'], ]; } // Add crashes per RC [$have_rc, $number_rc_builds] = $this->RCStatus(); if ($have_rc) { foreach (range(1, $number_rc_builds) as $rc_number) { $rc_number = (string) $this->release . '.0rc' . (string) $rc_number; if (defined('TESTING_CONTEXT')) { $rc_number = str_replace('94', '131', $rc_number); $target = URL::Socorro->target() . 'crash-stats.mozilla.org_' . $rc_number . '.json'; } else { $target = URL::Socorro->value . 'SuperSearch/?version=' . $rc_number . '&_facets=signature&product=Firefox'; // @codeCoverageIgnore } $temp = Json::load($target, 3600); // In local tests, we always have test data // @codeCoverageIgnoreStart if (empty($temp)) { $data[$rc_number] = [ 'total' => 0, 'signatures' => [], ]; continue; } // @codeCoverageIgnoreEnd $data[$rc_number] = [ 'total' => $temp['total'], 'signatures' => $temp['facets']['signature'], ]; } } // Create a summary of the crashes across betas $data['summary'] = [ 'total' => array_sum(array_column($data, 'total')), ]; return $data; } /** * Get the status of release candidates * * @return array<mixed> */ public function RCStatus() : array { if (defined('TESTING_CONTEXT')) { $shipping_build = 'Firefox-94.0-build1'; } else { $shipping_build = Json::load(URL::Balrog->value . 'rules/firefox-beta', 900)['mapping'] ?? 'N/A';// @codeCoverageIgnore } if ($shipping_build !== 'N/A') { // We have Release candidates [$product, $version, $build_number] = explode('-', (string) $shipping_build); $is_rc_build = ! str_contains($version, 'b'); $number_rc_builds = $is_rc_build ? (int) str_replace('build', '', $build_number) : 0; return [$is_rc_build, $number_rc_builds]; } return [false, 0];// @codeCoverageIgnore } }