blob: a68cfbe62b28eda34efdd3d3a1fba4a1d810cec7 [file] [edit]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {execSync} from 'node:child_process';
import {parseArgs} from 'node:util';
export function getLuciToken(): string {
try {
return execSync('luci-auth token', {encoding: 'utf-8'}).trim();
} catch {
console.error('Error: Could not get LUCI auth token. Did you run \'luci-auth login\'?');
process.exit(1);
}
}
interface ClusterMetric {
value: string;
}
interface ClusterMetrics {
'critical-failures-exonerated'?: ClusterMetric;
failures?: ClusterMetric;
'human-cls-failed-presubmit'?: ClusterMetric;
}
interface ClusterId {
id: string;
algorithm: string;
}
export interface Cluster {
title?: string;
clusterId?: ClusterId;
metrics?: ClusterMetrics;
bug?: {url?: string};
}
export async function getFlakyClusters(days = 1): Promise<Cluster[]> {
const url = 'https://analysis.api.luci.app/prpc/luci.analysis.v1.Clusters/QueryClusterSummaries';
const projectName = 'devtools-frontend';
const headers = {
Authorization: `Bearer ${getLuciToken()}`,
Accept: 'application/json',
'Content-Type': 'application/json',
};
const now = new Date();
const earliest = new Date(now.getTime() - (days * 24 * 60 * 60 * 1000));
const payload = {
project: projectName,
failureFilter: 'realm = "devtools-frontend:ci"',
orderBy: 'metrics.`failures`.value desc',
metrics: [
`projects/${projectName}/metrics/critical-failures-exonerated`,
`projects/${projectName}/metrics/failures`,
`projects/${projectName}/metrics/human-cls-failed-presubmit`,
],
timeRange: {
earliest: earliest.toISOString(),
latest: now.toISOString(),
},
};
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text();
console.error(`HTTP Error ${response.status}: ${response.statusText}`);
console.error(text);
throw new Error(`Network error querying LUCI Analysis: ${response.statusText}`);
}
let rawData = await response.text();
if (rawData.startsWith(')]}\'\n')) {
rawData = rawData.substring(5);
}
const data = JSON.parse(rawData);
let clusters: Cluster[] = data.clusterSummaries || [];
const isChromiumTest = (title: string) =>
title.includes('ninja://') || title.includes('Unexpected Diff') || title.includes('.cc(');
clusters = clusters.filter(c => !isChromiumTest(c.title || ''));
clusters = clusters.filter(c => Number(c.metrics?.failures?.value || 0) > 0);
return clusters;
}
async function main(): Promise<void> {
const {values} = parseArgs({
options: {
format: {
type: 'string',
},
},
});
const isJson = values.format === 'json';
if (!isJson) {
console.log('Fetching flaky clusters for devtools-frontend over the last 24 hours...');
}
const clusters = await getFlakyClusters(1);
if (isJson) {
console.log(JSON.stringify(clusters, null, 2));
} else {
console.log('\nFlaky Clusters for devtools-frontend:');
console.log('='.repeat(60));
for (const cluster of clusters) {
const title = cluster.title || 'No Title';
const clusterId = cluster.clusterId?.id || 'Unknown ID';
const algorithm = cluster.clusterId?.algorithm || 'Unknown Algorithm';
const totalFailures = cluster.metrics?.failures?.value || 0;
const clusterLink = `https://luci-analysis.appspot.com/p/devtools-frontend/clusters/${algorithm}/${clusterId}`;
console.log(`Cluster: ${title}`);
console.log(`Link: ${clusterLink}`);
console.log(`Impact: ${totalFailures} total failures`);
console.log('-'.repeat(60));
}
}
}
if (import.meta.main) {
main().catch(err => {
console.error(err);
process.exit(1);
});
}