Skip to content

Commit b10a32c

Browse files
committed
added Analyze Logs with Gemini button
1 parent aa8d05a commit b10a32c

7 files changed

Lines changed: 297 additions & 39 deletions

File tree

dashboard/lib/service/appengine_cocoon.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,43 @@ class AppEngineCocoonService implements CocoonService {
553553
);
554554
}
555555

556+
@override
557+
Future<CocoonResponse<void>> analyzeLogs({
558+
required String? idToken,
559+
required String repo,
560+
required int pr,
561+
required int buildId,
562+
String owner = 'flutter',
563+
}) async {
564+
if (idToken == null || idToken.isEmpty) {
565+
return const CocoonResponse<void>.error(
566+
'Sign in to analyze logs',
567+
statusCode: HttpStatus.unauthorized,
568+
);
569+
}
570+
571+
final analyzeUrl = apiEndpoint('/api/analyze-logs');
572+
final response = await _client.post(
573+
analyzeUrl,
574+
headers: {'X-Flutter-IdToken': idToken},
575+
body: jsonEncode({
576+
'owner': owner,
577+
'repo': repo,
578+
'pr': pr,
579+
'build_id': buildId,
580+
}),
581+
);
582+
583+
if (response.statusCode == HttpStatus.ok) {
584+
return const CocoonResponse.data(null);
585+
}
586+
587+
return CocoonResponse.error(
588+
'HTTP Code: ${response.statusCode}, ${response.body}',
589+
statusCode: response.statusCode,
590+
);
591+
}
592+
556593
@override
557594
Future<CocoonResponse<void>> rerunAllFailedJobs({
558595
required String? idToken,

dashboard/lib/service/cocoon.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ abstract class CocoonService {
8282
String owner = 'flutter',
8383
});
8484

85+
/// Analyze logs for the given failed job.
86+
Future<CocoonResponse<void>> analyzeLogs({
87+
required String? idToken,
88+
required String repo,
89+
required int pr,
90+
required int buildId,
91+
String owner = 'flutter',
92+
});
93+
8594
/// Schedule all failed tasks for the given [pr] to be re-run.
8695
Future<CocoonResponse<void>> rerunAllFailedJobs({
8796
required String? idToken,

dashboard/lib/service/data_seeder.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ class DataSeeder {
483483
attemptNumber: attemptNumber,
484484
creationTime: creationTime,
485485
buildNumber: 1337 + attemptNumber,
486+
buildId: 24567 + attemptNumber,
486487
summary: switch (status) {
487488
.succeeded =>
488489
'[INFO] Starting task $jobName...\n[SUCCESS] All tests passed (452/452)',
@@ -504,8 +505,6 @@ class DataSeeder {
504505
logAnalysis: switch (status) {
505506
.failed =>
506507
'Based on my analysis of the provided LUCI logs and the context of the changes in this PR, here is the breakdown of the build failure:\n\n ### 1. Identify the specific test or command that failed for $jobName \n...',
507-
.infraFailure =>
508-
'Based on my analysis of the provided LUCI logs and the context of the changes in this PR, here is the breakdown of the build failure:\n\n ### 1. Identify the specific infra issue for $jobName \n...',
509508
_ => null,
510509
},
511510
);

dashboard/lib/state/presubmit.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,33 @@ class PresubmitState extends ChangeNotifier {
535535
false;
536536
}
537537

538+
/// Whether the user can trigger log analysis for a specific job.
539+
bool canAnalyzeLog(PresubmitJobResponse job) {
540+
if (!authService.isAuthenticated || isLoading) {
541+
return false;
542+
}
543+
if (job.status != TaskStatus.failed &&
544+
job.status != TaskStatus.infraFailure) {
545+
return false;
546+
}
547+
return job.buildId != null &&
548+
(job.logAnalysis == null || job.logAnalysis!.trim().isEmpty);
549+
}
550+
551+
/// Triggers log analysis for a job.
552+
Future<String?> analyzeLogs(PresubmitJobResponse job) async {
553+
if (pr == null) return 'No PR selected';
554+
555+
final response = await cocoonService.analyzeLogs(
556+
idToken: await authService.idToken,
557+
repo: repo,
558+
pr: int.parse(pr!),
559+
buildId: job.buildId!,
560+
);
561+
562+
return response.error;
563+
}
564+
538565
void resume() {
539566
if (!_active) return;
540567
_startTimer();

dashboard/lib/views/presubmit_view.dart

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,9 @@ class _PreSubmitViewState extends State<PreSubmitView>
305305
'Select a job to view execution details.',
306306
),
307307
)
308-
: const _JobDetailsViewerPane(),
308+
: _JobDetailsViewerPane(
309+
onError: _showErrorDialog,
310+
),
309311
),
310312
],
311313
),
@@ -320,7 +322,9 @@ class _PreSubmitViewState extends State<PreSubmitView>
320322
}
321323

322324
class _JobDetailsViewerPane extends StatefulWidget {
323-
const _JobDetailsViewerPane();
325+
const _JobDetailsViewerPane({required this.onError});
326+
327+
final ValueChanged<String> onError;
324328

325329
@override
326330
State<_JobDetailsViewerPane> createState() => _JobDetailsViewerPaneState();
@@ -330,6 +334,7 @@ class _JobDetailsViewerPaneState extends State<_JobDetailsViewerPane> {
330334
int _selectedAttemptIndex = 0;
331335
int _selectedDetailTabIndex = 0;
332336
String? _lastJobName;
337+
bool _isAnalyzing = false;
333338

334339
@override
335340
Widget build(BuildContext context) {
@@ -368,7 +373,9 @@ class _JobDetailsViewerPaneState extends State<_JobDetailsViewerPane> {
368373
}
369374

370375
final selectedJob = jobs[_selectedAttemptIndex];
371-
final hasLogAnalysis = selectedJob.logAnalysis != null && selectedJob.logAnalysis!.isNotEmpty;
376+
final hasLogAnalysis =
377+
selectedJob.logAnalysis != null &&
378+
selectedJob.logAnalysis!.isNotEmpty;
372379

373380
return Column(
374381
crossAxisAlignment: CrossAxisAlignment.start,
@@ -499,8 +506,8 @@ class _JobDetailsViewerPaneState extends State<_JobDetailsViewerPane> {
499506
hasLogAnalysis && _selectedDetailTabIndex == 0
500507
? selectedJob.logAnalysis!
501508
: (selectedJob.summary?.trim().isEmpty ?? true
502-
? _getDefaultJobDetails(selectedJob)
503-
: selectedJob.summary!),
509+
? _getDefaultJobDetails(selectedJob)
510+
: selectedJob.summary!),
504511
style: const TextStyle(
505512
fontFamily: 'monospace',
506513
fontSize: 13,
@@ -511,43 +518,85 @@ class _JobDetailsViewerPaneState extends State<_JobDetailsViewerPane> {
511518
),
512519
Padding(
513520
padding: const EdgeInsets.all(24.0),
514-
child: ElevatedButton(
515-
onPressed: selectedJob.buildNumber == null
516-
? null
517-
: () async => await launchUrl(
518-
Uri.parse(
519-
generatePreSubmitBuildLogUrl(
520-
buildName: selectedJob.jobName,
521-
buildNumber: selectedJob.buildNumber!,
521+
child: Row(
522+
children: [
523+
ElevatedButton(
524+
onPressed: selectedJob.buildNumber == null
525+
? null
526+
: () async => await launchUrl(
527+
Uri.parse(
528+
generatePreSubmitBuildLogUrl(
529+
buildName: selectedJob.jobName,
530+
buildNumber: selectedJob.buildNumber!,
531+
),
532+
),
522533
),
534+
child: Row(
535+
mainAxisSize: MainAxisSize.min,
536+
children: [
537+
Icon(
538+
Icons.open_in_new,
539+
size: 18,
540+
color: selectedJob.buildNumber == null
541+
? Colors.grey
542+
: (isDark
543+
? const Color(0xFF58A6FF)
544+
: const Color(0xFF0969DA)),
523545
),
524-
),
525-
child: Row(
526-
mainAxisSize: MainAxisSize.min,
527-
children: [
528-
Icon(
529-
Icons.open_in_new,
530-
size: 18,
531-
color: selectedJob.buildNumber == null
532-
? Colors.grey
533-
: (isDark
534-
? const Color(0xFF58A6FF)
535-
: const Color(0xFF0969DA)),
546+
const SizedBox(width: 8),
547+
Text(
548+
'View more details on LUCI UI',
549+
style: TextStyle(
550+
color: selectedJob.buildNumber == null
551+
? Colors.grey
552+
: (isDark
553+
? const Color(0xFF58A6FF)
554+
: const Color(0xFF0969DA)),
555+
fontSize: 14,
556+
),
557+
),
558+
],
536559
),
537-
const SizedBox(width: 8),
538-
Text(
539-
'View more details on LUCI UI',
540-
style: TextStyle(
541-
color: selectedJob.buildNumber == null
542-
? Colors.grey
543-
: (isDark
544-
? const Color(0xFF58A6FF)
545-
: const Color(0xFF0969DA)),
546-
fontSize: 14,
560+
),
561+
if (presubmitState.canAnalyzeLog(selectedJob)) ...[
562+
const SizedBox(width: 16),
563+
ElevatedButton(
564+
onPressed: _isAnalyzing
565+
? null
566+
: () async {
567+
setState(() => _isAnalyzing = true);
568+
try {
569+
final error = await presubmitState.analyzeLogs(
570+
selectedJob,
571+
);
572+
if (error == null) {
573+
await presubmitState.fetchJobDetails();
574+
} else {
575+
widget.onError(error);
576+
}
577+
} finally {
578+
if (mounted) {
579+
setState(() => _isAnalyzing = false);
580+
}
581+
}
582+
},
583+
child: Row(
584+
mainAxisSize: MainAxisSize.min,
585+
children: [
586+
if (_isAnalyzing) ...[
587+
const SizedBox(
588+
width: 14,
589+
height: 14,
590+
child: CircularProgressIndicator(strokeWidth: 2),
591+
),
592+
const SizedBox(width: 8),
593+
],
594+
const Text('Analize Logs with Gemini'),
595+
],
547596
),
548597
),
549598
],
550-
),
599+
],
551600
),
552601
),
553602
],

dashboard/test/utils/mocks.mocks.dart

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)