Store nightly outcome and compute daily broken/recovered from Firebase (#53066)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53066

This diff introduces the mechanism for us to store the result of nightly 3p
library integration on Firebase.
Having the result store, we can now query the result from the previous day and
report if the build is newly broken or recovered overnight.

Changelog:
[Internal] [Changed] -

Reviewed By: cipolleschi

Differential Revision: D79565536

fbshipit-source-id: ba839b2950462a7ca6186a163f93f062719304fb
This commit is contained in:
Nicola Corti
2025-08-06 03:04:02 -07:00
committed by Facebook GitHub Bot
parent aa4555eaf1
commit f806851875
6 changed files with 957 additions and 16 deletions
+219
View File
@@ -0,0 +1,219 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
// We connect to firebase using a plain HTTP request because we don't want to
// add yet another devDependency to the react-native monorepo.
class FirebaseClient {
constructor() {
this.email = process.env.FIREBASE_APP_EMAIL;
this.password = process.env.FIREBASE_APP_PASS;
this.apiKey = process.env.FIREBASE_APP_APIKEY;
this.projectId = process.env.FIREBASE_APP_PROJECTNAME;
this.databaseUrl = `${this.projectId}-default-rtdb.firebaseio.com`;
this.idToken = null;
}
async authenticate() {
if (!this.email || !this.password) {
throw new Error(
'Firebase credentials not found in environment variables',
);
}
const authData = {
email: this.email,
password: this.password,
returnSecureToken: true,
};
const response = await this.makeRequest(
'identitytoolkit.googleapis.com',
`/v1/accounts:signInWithPassword?key=${this.apiKey}`,
'POST',
authData,
);
this.idToken = response.idToken;
return;
}
/**
* Make a database request for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @param {string} method - HTTP method
* @param {*} data - Data to send (optional)
* @returns {Promise<*>} - Response data
*/
async makeDatabaseRequest(date, method, data = null) {
if (!this.idToken) {
await this.authenticate();
}
const path = `/nightly-results/${date}.json?auth=${this.idToken}`;
return this.makeRequest(this.databaseUrl, path, method, data);
}
/**
* Store test results for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @param {Array<Object>} results - Array of test results
* @returns {Promise<void>}
*/
async storeResults(date, results) {
await this.makeDatabaseRequest(date, 'PUT', results);
console.log(`Successfully stored results for ${date}`);
}
/**
* Retrieve test results for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @returns {Promise<Array<Object>|null>} - Array of test results or null if not found
*/
async getResults(date) {
try {
return await this.makeDatabaseRequest(date, 'GET');
} catch (error) {
if (error.message.includes('404')) {
return null; // No results found for this specific date.
}
throw error;
}
}
async makeRequest(hostname, path, method, data = null) {
const url = `https://${hostname}${path}`;
const options = {
method,
headers: {
'Content-Type': 'application/json',
},
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
const responseText = await response.text();
if (!response.ok) {
let errorMessage;
try {
const parsedError = JSON.parse(responseText);
errorMessage = parsedError.error?.message || responseText;
} catch {
errorMessage = responseText;
}
throw new Error(`HTTP ${response.status}: ${errorMessage}`);
}
try {
return JSON.parse(responseText);
} catch {
return responseText;
}
}
}
/**
* Compare current results with previous day's results
* @param {Array<Object>} currentResults - Today's test results
* @param {Array<Object>} previousResults - Yesterday's test results
* @returns {Object} - Object containing broken and recovered tests
*/
function compareResults(currentResults, previousResults) {
if (!previousResults) {
return {
broken: [],
recovered: [],
newFailures: currentResults.filter(result => result.status !== 'success'),
};
}
// Create maps for easier lookup
const currentMap = new Map();
const previousMap = new Map();
currentResults.forEach(result => {
const key = `${result.library}-${result.platform}`;
currentMap.set(key, result);
});
previousResults.forEach(result => {
const key = `${result.library}-${result.platform}`;
previousMap.set(key, result);
});
const broken = [];
const recovered = [];
// Check for broken tests (was success, now failed)
for (const [key, currentResult] of currentMap) {
const previousResult = previousMap.get(key);
if (previousResult) {
if (
previousResult.status === 'success' &&
currentResult.status !== 'success'
) {
broken.push({
library: currentResult.library,
platform: currentResult.platform,
previousStatus: previousResult.status,
currentStatus: currentResult.status,
});
}
}
}
// Check for recovered tests (was failed, now success)
for (const [key, currentResult] of currentMap) {
const previousResult = previousMap.get(key);
if (previousResult) {
if (
previousResult.status !== 'success' &&
currentResult.status === 'success'
) {
recovered.push({
library: currentResult.library,
platform: currentResult.platform,
previousStatus: previousResult.status,
currentStatus: currentResult.status,
});
}
}
}
return {broken, recovered};
}
/**
* Get yesterday's date in YYYY-MM-DD format
* @returns {string} - Yesterday's date
*/
function getYesterdayDate() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return yesterday.toISOString().split('T')[0];
}
/**
* Get today's date in YYYY-MM-DD format
* @returns {string} - Today's date
*/
function getTodayDate() {
const today = new Date();
return today.toISOString().split('T')[0];
}
module.exports = {
FirebaseClient,
compareResults,
getYesterdayDate,
getTodayDate,
};