Skip to content

Commit 1f36150

Browse files
authored
Merge pull request #89 from shandli123/INTG_1575_JENKINS_REPORT
Intg 1575 Adding support to view bstack reports in jenkins CI
2 parents 914b236 + 7ec7273 commit 1f36150

File tree

13 files changed

+657
-4
lines changed

13 files changed

+657
-4
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</parent>
1111

1212
<artifactId>browserstack-integration</artifactId>
13-
<version>1.2.17-SNAPSHOT</version>
13+
<version>1.2.18-SNAPSHOT</version>
1414
<packaging>hpi</packaging>
1515

1616
<name>BrowserStack</name>

src/main/java/com/browserstack/automate/ci/common/BrowserStackEnvVars.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public interface BrowserStackEnvVars {
99
String BROWSERSTACK_LOCAL_IDENTIFIER = "BROWSERSTACK_LOCAL_IDENTIFIER";
1010
String BROWSERSTACK_BUILD = "BROWSERSTACK_BUILD";
1111
String BROWSERSTACK_BUILD_NAME = "BROWSERSTACK_BUILD_NAME";
12+
13+
String BROWSERSTACK_PROJECT_NAME = "BROWSERSTACK_PROJECT_NAME";
1214
String BROWSERSTACK_APP_ID = "BROWSERSTACK_APP_ID";
1315
String BROWSERSTACK_RERUN = "BROWSERSTACK_RERUN";
1416
String BROWSERSTACK_RERUN_TESTS = "BROWSERSTACK_RERUN_TESTS";

src/main/java/com/browserstack/automate/ci/common/constants/Constants.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ public class Constants {
1515
public static final String BROWSERSTACK_REPORT_PATH_PATTERN = "**/browserstack-artifacts/*";
1616
public static final String JENKINS_CI_PLUGIN = "JenkinsCiPlugin";
1717

18+
public static final String CAD_BASE_URL = "https://api-observability.browserstack.com/ext";
19+
public static final String BROWSERSTACK_CONFIG_DETAILS_ENDPOINT = "/v1/builds/buildReport";
20+
21+
public static final String INTEGRATIONS_TOOL_KEY = "jenkins";
22+
23+
public static final String BROWSERSTACK_TEST_REPORT_URL = "testReportBrowserStack";
24+
public static final String BROWSERSTACK_CAD_REPORT_DISPLAY_NAME = "BrowserStack Test Report and Insights";
25+
public static final String BROWSERSTACK_REPORT_FILENAME = "browserstack-report";
26+
public static final String BROWSERSTACK_REPORT_FOLDER = "browserstack-artifacts";
27+
28+
public static final String BROWSERSTACK_REPORT_AUT_PIPELINE_FUNCTION = "browserStackReportAut";
29+
1830
// Product
1931
public static final String AUTOMATE = "automate";
2032
public static final String APP_AUTOMATE = "app-automate";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.browserstack.automate.ci.jenkins.integrationService;
2+
3+
public enum BrowserStackReportStatus {
4+
IN_PROGRESS,
5+
COMPLETED,
6+
TEST_AVAILABLE,
7+
NOT_AVAILABLE
8+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package com.browserstack.automate.ci.jenkins.integrationService;
2+
3+
import com.browserstack.automate.ci.common.constants.Constants;
4+
import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
5+
import com.google.gson.Gson;
6+
import hudson.EnvVars;
7+
import hudson.FilePath;
8+
import hudson.model.Action;
9+
import hudson.model.Run;
10+
import hudson.model.TaskListener;
11+
import hudson.tasks.ArtifactArchiver;
12+
import org.json.JSONObject;
13+
import okhttp3.*;
14+
15+
import java.io.IOException;
16+
import java.io.PrintStream;
17+
import java.util.Arrays;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
import static com.browserstack.automate.ci.common.logger.PluginLogger.log;
22+
import static com.browserstack.automate.ci.common.logger.PluginLogger.logError;
23+
24+
public class BrowserStackTestReportAction implements Action {
25+
26+
private static final String DEFAULT_REPORT_TIMEOUT = "120";
27+
private static final String SUCCESS_REPORT = "SUCCESS_REPORT";
28+
private static final String REPORT_IN_PROGRESS = "REPORT_IN_PROGRESS";
29+
private static final String REPORT_FAILED = "REPORT_FAILED";
30+
private static final String RETRY_REPORT = "RETRY_REPORT";
31+
private static final String RATE_LIMIT = "RATE_LIMIT";
32+
private static final String TEST_AVAILABLE = "TEST_AVAILABLE";
33+
private static final int MAX_ATTEMPTS = 3;
34+
35+
private final transient PrintStream logger;
36+
private final RequestsUtil requestsUtil;
37+
private final BrowserStackCredentials credentials;
38+
private final String buildName;
39+
private final String buildCreatedAt;
40+
41+
private Run<?, ?> run;
42+
private String reportHtml;
43+
private String reportStyle;
44+
private String reportStatus;
45+
private int maxRetryReportAttempt;
46+
47+
public BrowserStackTestReportAction(Run<?, ?> run, BrowserStackCredentials credentials, String buildName, String buildCreatedAt, final PrintStream logger) {
48+
this.run = run;
49+
this.credentials = credentials;
50+
this.buildName = buildName;
51+
this.buildCreatedAt = buildCreatedAt;
52+
this.logger = logger;
53+
this.requestsUtil = new RequestsUtil();
54+
this.reportHtml = null;
55+
this.reportStyle = "";
56+
this.reportStatus = "";
57+
this.maxRetryReportAttempt = MAX_ATTEMPTS;
58+
}
59+
60+
public String getReportHtml() {
61+
ensureReportFetched();
62+
return reportHtml;
63+
}
64+
65+
public String getReportStyle() {
66+
ensureReportFetched();
67+
return reportStyle;
68+
}
69+
70+
private void ensureReportFetched() {
71+
if (!isReportCompletedOrFailed()) {
72+
fetchReport();
73+
}
74+
}
75+
76+
private boolean isReportCompletedOrFailed() {
77+
return reportStatus.equals(SUCCESS_REPORT) || reportStatus.equals(REPORT_FAILED);
78+
}
79+
80+
private void fetchReport() {
81+
Map<String, Object> params = createReportParams();
82+
String reportUrl = Constants.CAD_BASE_URL + Constants.BROWSERSTACK_CONFIG_DETAILS_ENDPOINT;
83+
84+
try {
85+
log(logger, "Fetching BrowserStack report...");
86+
Response response = requestsUtil.makeRequest(reportUrl, credentials, createRequestBody(params));
87+
handleResponse(response);
88+
} catch (Exception e) {
89+
if(!isReportTestAvailable()) {
90+
handleFetchException(e);
91+
}
92+
}
93+
}
94+
95+
private Map<String, Object> createReportParams() {
96+
String RquestTypeForJenkins = "POLL";
97+
Map<String, Object> params = new HashMap<>();
98+
params.put("buildStartedAt", buildCreatedAt);
99+
params.put("originalBuildName", buildName);
100+
params.put("requestingCi", Constants.INTEGRATIONS_TOOL_KEY);
101+
params.put("reportFormat", Arrays.asList("richHtml", "basicHtml"));
102+
params.put("requestType", RquestTypeForJenkins);
103+
params.put("userTimeout", DEFAULT_REPORT_TIMEOUT);
104+
return params;
105+
}
106+
107+
private RequestBody createRequestBody(Map<String, Object> params) {
108+
Gson gson = new Gson();
109+
String json = gson.toJson(params);
110+
return RequestBody.create(MediaType.parse("application/json"), json);
111+
}
112+
113+
private void handleResponse(Response response) throws Exception {
114+
if (response.isSuccessful()) {
115+
processSuccessfulResponse(response);
116+
} else {
117+
if(!isReportTestAvailable()) {
118+
if (response.code() == 429) {
119+
reportStatus = RATE_LIMIT;
120+
} else {
121+
reportStatus = REPORT_FAILED;
122+
logError(logger, "Non-success response while fetching report: " + response.code());
123+
}
124+
}
125+
}
126+
}
127+
128+
private void processSuccessfulResponse(Response response) throws Exception {
129+
assert response.body() != null;
130+
JSONObject reportResponse = new JSONObject(response.body().string());
131+
String responseReportStatus = reportResponse.optString("reportStatus");
132+
JSONObject report = reportResponse.optJSONObject("report");
133+
134+
switch (responseReportStatus.toUpperCase()) {
135+
case "COMPLETED":
136+
case "NOT_AVAILABLE":
137+
setReportSuccess(report);
138+
break;
139+
case "IN_PROGRESS":
140+
reportStatus = REPORT_IN_PROGRESS;
141+
break;
142+
case "TEST_AVAILABLE":
143+
setReportSuccess(report);
144+
reportStatus = TEST_AVAILABLE;
145+
break;
146+
default:
147+
reportStatus = REPORT_FAILED;
148+
}
149+
}
150+
151+
private void setReportSuccess(JSONObject report) {
152+
String defaultHTML = "<h1>No Report Found</h1>";
153+
reportStatus = SUCCESS_REPORT;
154+
reportHtml = report != null ? report.optString("richHtml", defaultHTML) : defaultHTML;
155+
reportStyle = report != null ? report.optString("richCss", "") : "";
156+
157+
try {
158+
String basicHtml = report != null ? report.optString("basicHtml", defaultHTML) : defaultHTML;
159+
String fullHtml = "<!DOCTYPE html> <html><head> <head>" + basicHtml + "</html>";
160+
161+
// Save the HTML content to a file in the workspace
162+
FilePath workspace = new FilePath(run.getRootDir()).getParent();
163+
ArtifactArchiver artifactArchiver = getArtifactArchiver(workspace, fullHtml);
164+
artifactArchiver.perform(run, workspace, new EnvVars(), null, TaskListener.NULL);
165+
} catch (Exception e) {
166+
logError(logger, "Failed to save or archive report artifact: " + e.getMessage());
167+
}
168+
}
169+
170+
private static ArtifactArchiver getArtifactArchiver(FilePath workspace, String fullHtml) throws IOException, InterruptedException {
171+
FilePath artifactsDir = new FilePath(workspace, Constants.BROWSERSTACK_REPORT_FOLDER);
172+
artifactsDir.mkdirs();
173+
String htmlFileName = Constants.BROWSERSTACK_REPORT_FILENAME + ".html";
174+
175+
FilePath htmlFile = new FilePath(artifactsDir, htmlFileName);
176+
177+
htmlFile.write(fullHtml, "UTF-8");
178+
String artifactFilePath = Constants.BROWSERSTACK_REPORT_FOLDER + "/" + htmlFileName;
179+
// Archive the file as an artifact
180+
ArtifactArchiver artifactArchiver = new ArtifactArchiver(artifactFilePath);
181+
artifactArchiver.setAllowEmptyArchive(false);
182+
return artifactArchiver;
183+
}
184+
185+
private void handleFetchException(Exception e) {
186+
reportStatus = RETRY_REPORT;
187+
maxRetryReportAttempt--;
188+
if (maxRetryReportAttempt < 0) {
189+
reportStatus = REPORT_FAILED;
190+
}
191+
logError(logger, "Exception while fetching the report: " + Arrays.toString(e.getStackTrace()));
192+
}
193+
194+
public boolean isReportInProgress() {
195+
return reportStatus.equals(REPORT_IN_PROGRESS);
196+
}
197+
198+
public boolean isReportFailed() {
199+
return reportStatus.equals(REPORT_FAILED);
200+
}
201+
202+
public boolean reportRetryRequired() {
203+
return reportStatus.equals(RETRY_REPORT);
204+
}
205+
206+
public boolean isUserRateLimited() {
207+
return reportStatus.equals(RATE_LIMIT);
208+
}
209+
210+
public boolean isReportAvailable() {
211+
return reportStatus.equals(SUCCESS_REPORT) || reportStatus.equals(TEST_AVAILABLE);
212+
}
213+
214+
public boolean isReportTestAvailable() {
215+
return reportStatus.equals(TEST_AVAILABLE);
216+
}
217+
218+
public boolean reportHasStatus() {
219+
return !reportStatus.isEmpty() && (reportStatus.equals(REPORT_IN_PROGRESS) || reportStatus.equals(REPORT_FAILED));
220+
}
221+
222+
public Run<?, ?> getBuild() {
223+
return run;
224+
}
225+
226+
public void setBuild(Run<?, ?> build) {
227+
this.run = build;
228+
}
229+
230+
@Override
231+
public String getIconFileName() {
232+
return Constants.BROWSERSTACK_LOGO;
233+
}
234+
235+
@Override
236+
public String getDisplayName() {
237+
return Constants.BROWSERSTACK_REPORT_DISPLAY_NAME;
238+
}
239+
240+
@Override
241+
public String getUrlName() {
242+
return Constants.BROWSERSTACK_TEST_REPORT_URL;
243+
}
244+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.browserstack.automate.ci.jenkins.integrationService;
2+
3+
import com.browserstack.automate.ci.common.BrowserStackEnvVars;
4+
import com.browserstack.automate.ci.common.constants.Constants;
5+
import com.browserstack.automate.ci.jenkins.BrowserStackBuildAction;
6+
import com.browserstack.automate.ci.jenkins.BrowserStackCredentials;
7+
import edu.umd.cs.findbugs.annotations.NonNull;
8+
import hudson.EnvVars;
9+
import hudson.Extension;
10+
import hudson.model.AbstractProject;
11+
import hudson.model.Run;
12+
import hudson.model.TaskListener;
13+
import hudson.tasks.BuildStepDescriptor;
14+
import hudson.tasks.BuildStepMonitor;
15+
import hudson.tasks.Publisher;
16+
import hudson.tasks.Recorder;
17+
import hudson.FilePath;
18+
import hudson.Launcher;
19+
import jenkins.tasks.SimpleBuildStep;
20+
import org.jenkinsci.Symbol;
21+
import org.kohsuke.stapler.DataBoundConstructor;
22+
23+
import javax.annotation.CheckForNull;
24+
import java.io.IOException;
25+
import java.io.PrintStream;
26+
import java.util.*;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.logging.Logger;
29+
30+
import static com.browserstack.automate.ci.common.logger.PluginLogger.log;
31+
import static com.browserstack.automate.ci.common.logger.PluginLogger.logError;
32+
33+
public class BrowserStackTestReportPublisher extends Recorder implements SimpleBuildStep {
34+
private static final Logger LOGGER = Logger.getLogger(BrowserStackTestReportPublisher.class.getName());
35+
private Map<String, String> customEnvVars;
36+
37+
@DataBoundConstructor
38+
public BrowserStackTestReportPublisher(@CheckForNull String product) {
39+
this.customEnvVars = new ConcurrentHashMap<>();
40+
}
41+
42+
@Override
43+
public void perform(Run<?, ?> build, @NonNull FilePath workspace, @NonNull Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
44+
final PrintStream logger = listener.getLogger();
45+
log(logger, "Adding BrowserStack Report");
46+
47+
EnvVars parentEnvs = build.getEnvironment(listener);
48+
parentEnvs.putAll(getCustomEnvVars());
49+
50+
String browserStackBuildName = Optional.ofNullable(parentEnvs.get(BrowserStackEnvVars.BROWSERSTACK_BUILD_NAME))
51+
.orElse(parentEnvs.get(Constants.JENKINS_BUILD_TAG));
52+
53+
BrowserStackBuildAction buildAction = build.getAction(BrowserStackBuildAction.class);
54+
if (buildAction == null || buildAction.getBrowserStackCredentials() == null) {
55+
logError(logger, "No BrowserStackBuildAction or credentials found");
56+
return;
57+
}
58+
59+
BrowserStackCredentials credentials = buildAction.getBrowserStackCredentials();
60+
61+
LOGGER.info("Adding BrowserStack Report Action");
62+
63+
64+
Date buildTimestamp = new Date(build.getStartTimeInMillis());
65+
66+
// Format the timestamp (e.g., YYYY-MM-DD HH:MM:SS)
67+
long unixTimestamp = buildTimestamp.getTime() / 1000;
68+
69+
String buildCreatedAt = String.valueOf(unixTimestamp);
70+
71+
build.addAction(new BrowserStackTestReportAction(build, credentials, browserStackBuildName,buildCreatedAt, logger));
72+
73+
}
74+
75+
76+
public Map<String, String> getCustomEnvVars() {
77+
return customEnvVars;
78+
}
79+
80+
@Override
81+
public BuildStepMonitor getRequiredMonitorService() {
82+
return BuildStepMonitor.NONE;
83+
}
84+
85+
@Symbol(Constants.BROWSERSTACK_REPORT_PIPELINE_FUNCTION)
86+
@Extension
87+
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
88+
89+
@Override
90+
@SuppressWarnings("rawtypes")
91+
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
92+
// indicates that this builder can be used with all kinds of project types
93+
return true;
94+
}
95+
@Override
96+
public String getDisplayName() {
97+
return Constants.BROWSERSTACK_CAD_REPORT_DISPLAY_NAME;
98+
}
99+
100+
}
101+
}

0 commit comments

Comments
 (0)