Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
Expand Up @@ -65,6 +65,8 @@ public static final class SessionStatus {
public static final class QualityDashboardAPI {
public static final String QEI_DEFAULT_URL = "https://quality-engineering-insights.browserstack.com";
public static String host = QEI_DEFAULT_URL;
// Cache configuration
public static final long CACHE_DURATION_MS = 60 * 60 * 1000L; // 1 hour in milliseconds

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok for now but ideally it should be configurable either on client side or server side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. As discussed, we will handle this in the future.


public static String getHost() {
return host;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,29 @@
import hudson.Extension;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.Run;
import java.sql.Timestamp;
import java.time.Instant;
import java.io.Serializable;
import jenkins.model.Jenkins;
import okhttp3.*;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import java.time.temporal.ChronoUnit;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

@Extension
public class QualityDashboardInit {

private static final Logger LOGGER = Logger.getLogger(QualityDashboardInit.class.getName());
static QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();

@Initializer(after = InitMilestone.PLUGINS_PREPARED)
@Initializer(after = InitMilestone.JOB_LOADED)
public static void postInstall() {
try {
initQDSetupIfRequired();
Expand All @@ -51,13 +55,15 @@ private static String exceptionToString(Throwable throwable) {
private static void initQDSetupIfRequired() throws JsonProcessingException {
BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
try {
if(browserStackCredentials!=null) {
if(browserStackCredentials != null) {
apiUtil.logToQD(browserStackCredentials,"Starting plugin data export to QD");
checkQDIntegrationAndDumpMetaData(browserStackCredentials);
} else {
LOGGER.info("BrowserStack credentials not found. Skipping Quality Dashboard initialization.");
}
} catch (Exception e) {
try {
apiUtil.logToQD(browserStackCredentials, "Global exception in data export is:");
apiUtil.logToQD(browserStackCredentials, "Global exception in data export is:" + exceptionToString(e));
} catch (Exception ex) {
String exceptionString = exceptionToString(ex);
apiUtil.logToQD(browserStackCredentials, "Global exception in exception data export is:" + exceptionString);
Expand All @@ -69,7 +75,7 @@ private static void initQDSetupIfRequired() throws JsonProcessingException {

private static void checkQDIntegrationAndDumpMetaData(BrowserStackCredentials browserStackCredentials) throws JsonProcessingException {
if(initialQDSetupRequired(browserStackCredentials)) {
List<String> allPipelines = getAllPipelines(browserStackCredentials);
List<PipelineInfo> allPipelines = getAllPipelines(browserStackCredentials);
if(!allPipelines.isEmpty()){
boolean projectsSavedSuccessfully = sendPipelinesPaginated(browserStackCredentials, allPipelines);
if(projectsSavedSuccessfully) {
Expand Down Expand Up @@ -105,41 +111,47 @@ private static boolean initialQDSetupRequired(BrowserStackCredentials browserSta
return false;
}

private static List<String> getAllPipelines(BrowserStackCredentials browserStackCredentials) {
List<String> allPipelines = new ArrayList<>();
private static List<PipelineInfo> getAllPipelines(BrowserStackCredentials browserStackCredentials) {
List<PipelineInfo> allPipelines = new ArrayList<>();
Jenkins jenkins = Jenkins.getInstanceOrNull();
Integer totalPipelines = 0;

if (jenkins != null) {
totalPipelines = jenkins.getAllItems().size();
jenkins.getAllItems().forEach(job -> {
try {
String itemType = QualityDashboardUtil.getItemTypeModified(job);
boolean isWorkflowJob = job instanceof WorkflowJob;

// Logging job details
apiUtil.logToQD(
browserStackCredentials,
String.format(
"Job name: %s, instance type: %s, and is_workflow_job: %s",
job.getName(),
job.getClass().getSimpleName(),
(job instanceof WorkflowJob) ? "yes" : "no"
itemType,
isWorkflowJob ? "yes" : "no"
)
);
if (itemType != null) {
String pipelineName = job.getFullName();
allPipelines.add(new PipelineInfo(pipelineName, itemType));
}
else{
apiUtil.logToQD(browserStackCredentials, "Skipping job: " + job.getName() + " as it is not a Job or Folder instance");
}

} catch (JsonProcessingException e) {
// Handling the exception and logging an error
System.err.println("Error processing JSON for job: " + job.getName());
LOGGER.info("Error processing JSON for job: " + job.getName() + " - " + e.getMessage());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be logger error right? And till will also be displayed to user correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karanshah-browserstack , yes, the user will be able to see it in the Jenkins System Log. I've changed it to a warning in all places.

e.printStackTrace();
}

if (job instanceof WorkflowJob) {
String pipelineName = job.getFullName(); // Getting pipeline name
allPipelines.add(pipelineName);
}
});
} else {
try {
apiUtil.logToQD(browserStackCredentials, "Issue getting Jenkins Instance");
} catch (JsonProcessingException e) {
System.err.println("Error logging issue with Jenkins instance.");
LOGGER.info("Error logging issue with Jenkins instance.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same error Logger comment as above for all logger related changes

e.printStackTrace();
}
}
Expand All @@ -149,20 +161,20 @@ private static List<String> getAllPipelines(BrowserStackCredentials browserStack
apiUtil.logToQD(browserStackCredentials,"Total Pipelines detected : " + allPipelines.size());
} catch (JsonProcessingException e) {
// Handling the exception and logging an error
System.err.println("Error processing JSON for total pipelines: ");
LOGGER.info("Error processing JSON for total pipelines: " + e.getMessage());
e.printStackTrace();
}
// Returning the list of filtered pipelines
return allPipelines;
}

private static boolean sendPipelinesPaginated(BrowserStackCredentials browserStackCredentials, List<String> allPipelines) {
private static boolean sendPipelinesPaginated(BrowserStackCredentials browserStackCredentials, List<PipelineInfo> allPipelines) {
boolean isSuccess = true;
int pageSize = getProjectPageSize(browserStackCredentials);
List<List<String>> pipelinesInSmallerBatches = Lists.partition(allPipelines, pageSize);
List<List<PipelineInfo>> pipelinesInSmallerBatches = Lists.partition(allPipelines, pageSize);
int totalPages = !pipelinesInSmallerBatches.isEmpty() ? pipelinesInSmallerBatches.size() : 0;
int page = 0;
for(List<String> singlePagePipelineList : pipelinesInSmallerBatches) {
for(List<PipelineInfo> singlePagePipelineList : pipelinesInSmallerBatches) {
try {
page++;
ObjectMapper objectMapper = new ObjectMapper();
Expand All @@ -188,10 +200,12 @@ private static List<PipelineDetails> getAllBuilds(BrowserStackCredentials browse
Jenkins jenkins = Jenkins.getInstanceOrNull();
Instant thresholdInstant = Instant.now().minus(getHistoryForDays(browserStackCredentials), ChronoUnit.DAYS);
if (jenkins != null) {
jenkins.getAllItems().forEach(job -> {
if (job instanceof WorkflowJob) {
jenkins.getAllItems().forEach(item -> {
// Support both WorkflowJob and Matrix projects (and potentially other job types)
if (item instanceof Job) {
Job<?, ?> job = (Job<?, ?>) item;
String pipelineName = job.getFullName();
List<WorkflowRun> allBuilds = ((WorkflowJob) job).getBuilds();
List<? extends Run<?, ?>> allBuilds = job.getBuilds();
if(!allBuilds.isEmpty()) {
allBuilds.stream().filter(build -> Instant.ofEpochMilli(build.getTimeInMillis()).isAfter(thresholdInstant)).forEach(
build -> {
Expand All @@ -201,7 +215,19 @@ private static List<PipelineDetails> getAllBuilds(BrowserStackCredentials browse
long endTimeInMillis = build.getTimeInMillis();
Timestamp endTime = new Timestamp(endTimeInMillis);
String result = overallResult != null ? overallResult.toString() : null;
PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result, endTime);

// Get root upstream project information for QEI with build number (returns in format "project#build")
String rootUpstreamProject = "";
String immediateParentProject = "";
try {
rootUpstreamProject = UpstreamPipelineResolver.resolveRootUpstreamProject(build, browserStackCredentials);
immediateParentProject = UpstreamPipelineResolver.resolveImmediateUpstreamProjectForQEI(build, browserStackCredentials);
} catch (Exception e) {
LOGGER.info("Error resolving upstream project for " + pipelineName + " build number " + buildNumber + ": " + e.getMessage());
e.printStackTrace();
}
PipelineDetails pipelineDetail = new PipelineDetails(pipelineName, buildNumber, duration, result,
endTime, rootUpstreamProject, immediateParentProject);
allBuildResults.add(pipelineDetail);
}
);
Expand Down Expand Up @@ -249,9 +275,8 @@ private static int getHistoryForDays(BrowserStackCredentials browserStackCredent
}
} catch(IOException e) {
e.printStackTrace();
} finally {
return no_of_days;
}
return no_of_days;
}

private static int getProjectPageSize(BrowserStackCredentials browserStackCredentials) {
Expand All @@ -268,9 +293,8 @@ private static int getProjectPageSize(BrowserStackCredentials browserStackCreden
}
} catch(IOException e) {
e.printStackTrace();
} finally {
return projectPageSize;
}
return projectPageSize;
}

private static int getResultPageSize(BrowserStackCredentials browserStackCredentials) {
Expand All @@ -287,9 +311,8 @@ private static int getResultPageSize(BrowserStackCredentials browserStackCredent
}
} catch(IOException e) {
e.printStackTrace();
} finally {
return resultPageSize;
}
return resultPageSize;
}
}

Expand All @@ -307,13 +330,35 @@ class PipelineDetails {

@JsonProperty("endTime")
private Timestamp endTime;

public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus, Timestamp endTime) {

@JsonProperty("rootProject")
private String rootProject;

@JsonProperty("immediateParentProject")
private String immediateParentProject;

public PipelineDetails(String pipelineName, Integer buildNumber, Long buildDuration, String buildStatus,
Timestamp endTime, String rootProject, String immediateParentProject) {
this.pipelineName = pipelineName;
this.buildNumber = buildNumber;
this.buildDuration = buildDuration;
this.buildStatus = buildStatus;
this.endTime = endTime;
this.rootProject = rootProject;
this.immediateParentProject = immediateParentProject;
}
}

class PipelineInfo implements Serializable {
@JsonProperty("pipelineName")
private String pipelineName;

@JsonProperty("jobType")
private String jobType;

public PipelineInfo(String pipelineName, String jobType) {
this.pipelineName = pipelineName;
this.jobType = jobType;
}
}

Expand All @@ -325,9 +370,9 @@ class PipelinesPaginated {
private int totalPages;

@JsonProperty("pipelines")
private List<String> pipelines;
private List<PipelineInfo> pipelines;

public PipelinesPaginated(int page, int totalPages, List<String> pipelines) {
public PipelinesPaginated(int page, int totalPages, List<PipelineInfo> pipelines) {
this.page = page;
this.totalPages = totalPages;
this.pipelines = pipelines;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,35 @@
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import java.io.IOException;
import java.io.Serializable;
import java.util.logging.Logger;

@Extension
public class QualityDashboardInitItemListener extends ItemListener {

private static final Logger LOGGER = Logger.getLogger(QualityDashboardInitItemListener.class.getName());

@Override
public void onCreated(Item job) {
try {
BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
if(browserStackCredentials == null) {
LOGGER.info("BrowserStackCredentials not found. Please ensure they are configured correctly.");
return;
}
QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
apiUtil.logToQD(browserStackCredentials, "Item Created : " + job.getClass().getName()) ;

String itemName = job.getFullName();
String itemType = getItemTypeModified(job);
String itemType = QualityDashboardUtil.getItemTypeModified(job);

apiUtil.logToQD(browserStackCredentials, "Item Type : " + itemType + " : " + itemName + " : " + itemType.equals("PIPELINE"));
if(itemType != null && itemType.equals("PIPELINE")) {
apiUtil.logToQD(browserStackCredentials, "Item Created : " + itemName + " - " + "Item Type : " + itemType);
if(itemType != null) {
try {
apiUtil.logToQD(browserStackCredentials, "Item Type inside the Folder : " + itemType + " : " + itemName + " : " + itemType.equals("PIPELINE"));
String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType));
syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "POST");

} catch(IOException e) {
} catch(Exception e) {
LOGGER.info("Error syncing item creation to Quality Dashboard: " + e.getMessage());
e.printStackTrace();
}
}
Expand All @@ -47,42 +51,34 @@ public void onCreated(Item job) {
@Override
public void onDeleted(Item job) {
String itemName = job.getFullName();
String itemType = getItemTypeModified(job);
String itemType = QualityDashboardUtil.getItemTypeModified(job);
if(itemType != null) {
try {
String jsonBody = getJsonReqBody(new ItemUpdate(itemName, itemType));
syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "DELETE");
} catch(IOException e) {
} catch(Exception e) {
LOGGER.info("Error syncing item deletion to Quality Dashboard: " + e.getMessage());
e.printStackTrace();
}
}
}

@Override
public void onRenamed(Item job, String oldName, String newName) {
String itemType = getItemTypeModified(job);
String itemType = QualityDashboardUtil.getItemTypeModified(job);
if(itemType != null) {
try {
oldName = job.getParent().getFullName() + "/" + oldName;
newName = job.getParent().getFullName() + "/" + newName;
String jsonBody = getJsonReqBody(new ItemRename(oldName, newName, itemType));
syncItemListToQD(jsonBody, Constants.QualityDashboardAPI.getItemCrudEndpoint(), "PUT");
} catch(IOException e) {
} catch(Exception e) {
LOGGER.info("Error syncing item rename to Quality Dashboard: " + e.getMessage());
e.printStackTrace();
}
}
}

private String getItemTypeModified(Item job) {
String itemType = null;
boolean isFolderRenamed = job.getClass().getName().contains("Folder");
boolean isPipelineRenamed = job instanceof WorkflowJob;
if(isFolderRenamed || isPipelineRenamed) {
itemType = isPipelineRenamed ? "PIPELINE" : "FOLDER";
}
return itemType;
}

private <T> String getJsonReqBody( T item) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String jsonBody = objectMapper.writeValueAsString(item);
Expand All @@ -93,6 +89,10 @@ private Response syncItemListToQD(String jsonBody, String url, String typeOfRequ
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), jsonBody);
QualityDashboardAPIUtil apiUtil = new QualityDashboardAPIUtil();
BrowserStackCredentials browserStackCredentials = QualityDashboardUtil.getBrowserStackCreds();
if(browserStackCredentials == null) {
LOGGER.info("BrowserStack credentials not found. Please ensure they are configured correctly.");
return null;
}
if(typeOfRequest.equals("PUT")) {
apiUtil.logToQD(browserStackCredentials, "Syncing Item Update - PUT");
return apiUtil.makePutRequestToQd(url, browserStackCredentials, requestBody);
Expand Down
Loading