Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.jenkins.plugins.explain_error;

import com.google.common.annotations.VisibleForTesting;
import hudson.model.Result;
import hudson.model.Run;
import jenkins.model.RunAction2;
Expand Down Expand Up @@ -86,17 +85,13 @@
if (!forceNew && existingAction != null && existingAction.hasValidExplanation()) {
recordUsage(UsageEvent.Result.CACHE_HIT, existingAction.getProviderName(),
existingAction.getProviderModel(), startTimeNanos, existingAction.getInputLogLineCount());
// Return existing explanation with a flag indicating it's cached
writeJsonResponse(rsp, "success", existingAction.getProviderName(), createCachedResponse(existingAction.getExplanation()));
// Return existing explanation
writeJsonResponse(rsp, "success", existingAction.getProviderName(), existingAction.getExplanation());
return;
}

// Optionally allow maxLines as a parameter, default to 200
int maxLines = 200;
String maxLinesParam = req.getParameter("maxLines");
if (maxLinesParam != null) {
try { maxLines = Integer.parseInt(maxLinesParam); } catch (NumberFormatException ignore) {}
}
int maxLines = parseMaxLines(req, 200);

// Fetch the last N lines of the log
PipelineLogExtractor logExtractor = new PipelineLogExtractor(run, maxLines, Jenkins.getAuthentication2(),
Expand All @@ -120,6 +115,100 @@
}
}

/**
* AJAX endpoint to explain a selected Pipeline Graph View node.
*/
@RequirePOST
public void doExplainNodeError(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
long startTimeNanos = System.nanoTime();
String nodeId = req.getParameter("nodeId");
nodeId = nodeId != null ? nodeId.trim() : "";

try {
run.checkPermission(hudson.model.Item.READ);

if (nodeId.isEmpty()) {
recordUsage(UsageEvent.EntryPoint.PIPELINE_GRAPH_NODE, UsageEvent.Result.DISABLED,
null, null, startTimeNanos, 0);
writeJsonResponse(rsp, "warning", "Unknown", "No Pipeline node was selected.");
return;
}

boolean forceNew = "true".equals(req.getParameter("forceNew"));
StepErrorExplanationAction stepAction = run.getAction(StepErrorExplanationAction.class);
StepErrorExplanationAction.Entry existing = stepAction != null ? stepAction.getExplanation(nodeId) : null;
if (!forceNew && existing != null && existing.hasValidExplanation()) {

Check warning on line 140 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 140 is only partially covered, one branch is missing
this.urlString = existing.getUrlString();
recordUsage(UsageEvent.EntryPoint.PIPELINE_GRAPH_NODE, UsageEvent.Result.CACHE_HIT,
existing.getProviderName(), existing.getProviderModel(), startTimeNanos,
existing.getInputLogLineCount());
writeJsonResponse(rsp, "success", existing.getProviderName(), existing.getExplanation());
return;
}

int maxLines = parseMaxLines(req, 200);
PipelineLogExtractor logExtractor = new PipelineLogExtractor(run, maxLines, Jenkins.getAuthentication2(),
false, null);
PipelineLogExtractor.ExtractionResult extractionResult = logExtractor.extractNodeLog(nodeId);
this.urlString = extractionResult.url();
if (extractionResult.logLines().isEmpty()) {

Check warning on line 154 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 154 is only partially covered, one branch is missing
recordUsage(UsageEvent.EntryPoint.PIPELINE_GRAPH_NODE, UsageEvent.Result.DISABLED,
null, null, startTimeNanos, 0);
writeJsonResponse(rsp, "warning", "Unknown",
"No log output found for the selected Pipeline node.");
return;

Check warning on line 159 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 155-159 are not covered by tests
}

String errorText = String.join("\n", extractionResult.logLines());
ErrorExplainer explainer = new ErrorExplainer();
try {
ErrorExplanationAction explanation = explainer.explainErrorText(errorText, urlString, run,
UsageEvent.EntryPoint.PIPELINE_GRAPH_NODE, false);
StepErrorExplanationAction cacheAction = getOrCreateStepAction();
cacheAction.putExplanation(nodeId, explanation);
run.save();
writeJsonResponse(rsp, "success", explanation.getProviderName(), explanation.getExplanation());
} catch (ExplanationException ee) {
writeJsonResponse(rsp, ee.getLevel(), explainer.getProviderName(), ee.getMessage());

Check warning on line 172 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 171-172 are not covered by tests
}
} catch (Exception e) {
LOGGER.severe("=== EXPLAIN PIPELINE NODE REQUEST FAILED ===");
LOGGER.severe("Error explaining pipeline node: " + e.getMessage());
writeJsonResponse(rsp, "error", "Unknown", "Error: " + e.getMessage());

Check warning on line 177 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 174-177 are not covered by tests
}
}

/**
* AJAX endpoint to retrieve a previously generated explanation for a Pipeline node.
* Only returns cached results; never triggers AI generation.
*/
@RequirePOST
public void doGetNodeExplanation(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
String nodeId = req.getParameter("nodeId");
nodeId = nodeId != null ? nodeId.trim() : "";

try {
run.checkPermission(hudson.model.Item.READ);

if (nodeId.isEmpty()) {
writeJsonResponse(rsp, "warning", "Unknown", "No Pipeline node was selected.");
return;
}

StepErrorExplanationAction stepAction = run.getAction(StepErrorExplanationAction.class);
StepErrorExplanationAction.Entry entry = stepAction != null ? stepAction.getExplanation(nodeId) : null;
if (entry != null && entry.hasValidExplanation()) {

Check warning on line 200 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 200 is only partially covered, one branch is missing
this.urlString = entry.getUrlString();
writeJsonResponse(rsp, "success", entry.getProviderName(), entry.getExplanation());
} else {
writeJsonResponse(rsp, "no_cache", "Unknown", "");
}
} catch (Exception e) {
LOGGER.warning("Error getting node explanation: " + e.getMessage());
writeJsonResponse(rsp, "error", "Unknown", "Error: " + e.getMessage());

Check warning on line 208 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 206-208 are not covered by tests
}
}

/**
* AJAX endpoint to check build status.
* Returns JSON with buildingStatus to determine if button should be shown. 0 - SUCCESS, 1 - RUNNING, 2 - FINISHED and FAILURE
Expand All @@ -129,15 +218,11 @@
try {
run.checkPermission(hudson.model.Item.READ);

Integer buildingStatus = run.isBuilding() ? 1 : 0;

if (buildingStatus == 0) {
Result result = run.getResult();
if (result == Result.SUCCESS) {
buildingStatus = 0;
} else {
buildingStatus = 2;
}
int buildingStatus = 0;
if (run.isBuilding()) {
buildingStatus = 1;
} else if (run.getResult() == Result.FAILURE) {
buildingStatus = 2;
}

rsp.setContentType("application/json");
Expand Down Expand Up @@ -167,30 +252,49 @@
writer.flush();
}

/**
* Create a response indicating this is a cached result.
* @param explanation The cached explanation
* @return The response string with cached indicator
*/
@VisibleForTesting
String createCachedResponse(String explanation) {
return explanation + "\n\n[Note: This is a previously generated explanation. Use the 'Generate New' option to create a new one.]";
}

public Run<?, ?> getRun() {
return run;
}

private void recordUsage(UsageEvent.Result result, String providerName, String model,
long startTimeNanos, int inputLogLineCount) {
recordUsage(UsageEvent.EntryPoint.CONSOLE_ACTION, result, providerName, model,
startTimeNanos, inputLogLineCount);
}

private void recordUsage(UsageEvent.EntryPoint entryPoint, UsageEvent.Result result, String providerName,
String model, long startTimeNanos, int inputLogLineCount) {
UsageRecorders.get().record(new UsageEvent(
System.currentTimeMillis(),
UsageEvent.EntryPoint.CONSOLE_ACTION,
entryPoint,
result,
providerName,
model,
Math.max(0L, (System.nanoTime() - startTimeNanos) / 1_000_000L),
inputLogLineCount,
false));
}

private StepErrorExplanationAction getOrCreateStepAction() {
StepErrorExplanationAction action = run.getAction(StepErrorExplanationAction.class);
if (action != null) {
return action;
}
action = new StepErrorExplanationAction();
run.addAction(action);
return action;
}

private int parseMaxLines(StaplerRequest2 req, int defaultValue) {
String maxLinesParam = req.getParameter("maxLines");
if (maxLinesParam == null) {

Check warning on line 290 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 290 is only partially covered, one branch is missing
return defaultValue;
}
try {
int value = Integer.parseInt(maxLinesParam);
return value > 0 ? value : defaultValue;
} catch (NumberFormatException ignore) {
return defaultValue;

Check warning on line 297 in src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 294-297 are not covered by tests
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hudson.Extension;
import hudson.model.PageDecorator;
import hudson.model.Run;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;

Expand Down Expand Up @@ -39,13 +40,26 @@
}

/**
* Helper method used by jelly to checked if we're on a console url.
* Helper method used by jelly to check if we're on a supported build page.
*/
public boolean isPluginActive() {
return isConsolePage() || isPipelineGraphViewPage();

Check warning on line 46 in src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 46 is only partially covered, one branch is missing
}

public boolean isConsolePage() {
String uri = Stapler.getCurrentRequest2().getRequestURI();
return uri.matches(".*/console(Full)?$");
}

public boolean isPipelineGraphViewPage() {
String uri = Stapler.getCurrentRequest2().getRequestURI();
return isPipelineGraphViewUri(uri) && Jenkins.get().getPlugin("pipeline-graph-view") != null;

Check warning on line 56 in src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 56 is only partially covered, 3 branches are missing
}

static boolean isPipelineGraphViewUri(String uri) {
return uri != null && uri.matches(".*/(stages|pipeline-overview)/?$");

Check warning on line 60 in src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 60 is only partially covered, one branch is missing
}

public String getRunUrl() {
Ancestor ancestor = Stapler.getCurrentRequest2().findAncestor(Run.class);
if (ancestor != null && ancestor.getObject() instanceof Run<?, ?> run) {
Expand All @@ -55,7 +69,15 @@
}
}

/**
* Returns the console-level explanation for pre-populating the Jelly view.
* Only relevant on console pages; returns null on graph view pages because
* graph view uses per-node {@link StepErrorExplanationAction} explanations.
*/
public ErrorExplanationAction getExistingExplanation() {
if (isPipelineGraphViewPage()) {

Check warning on line 78 in src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 78 is only partially covered, one branch is missing
return null;

Check warning on line 79 in src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 79 is not covered by tests
}
Ancestor ancestor = Stapler.getCurrentRequest2().findAncestor(Run.class);
if (ancestor != null && ancestor.getObject() instanceof Run<?, ?> run) {
return run.getAction(ErrorExplanationAction.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ public ErrorExplanationAction explainErrorText(String errorText, String url, @No
ErrorExplanationAction explainErrorText(String errorText, String url, @NonNull Run<?, ?> run,
UsageEvent.EntryPoint entryPoint)
throws IOException, ExplanationException {
return explainErrorText(errorText, url, run, entryPoint, true);
}

ErrorExplanationAction explainErrorText(String errorText, String url, @NonNull Run<?, ?> run,
UsageEvent.EntryPoint entryPoint, boolean storeBuildAction)
throws IOException, ExplanationException {
String jobInfo ="[" + run.getParent().getFullName() + " #" + run.getNumber() + "]";
long startTimeNanos = System.nanoTime();
int inputLogLineCount = countLines(errorText);
Expand Down Expand Up @@ -289,8 +295,10 @@ ErrorExplanationAction explainErrorText(String errorText, String url, @NonNull R
LOGGER.fine("Explanation length: " + explanation.length());
ErrorExplanationAction action = new ErrorExplanationAction(explanation, url, errorText,
provider.getProviderName(), provider.getModel(), inputLogLineCount);
run.addOrReplaceAction(action);
run.save();
if (storeBuildAction) {
run.addOrReplaceAction(action);
run.save();
}
recordUsage(entryPoint, UsageEvent.Result.SUCCESS, provider, startTimeNanos, inputLogLineCount, false);

return action;
Expand Down
Loading