Proton Cloud SDK for Java
The br.com.protoncloud:protoncloud-sdk artifact, for Java 21 or later and Maven 3.9 or later. The Java
packages are br.com.atomicsolutions.proton and br.com.atomicsolutions.http.
Installation
<dependency>
<groupId>br.com.protoncloud</groupId>
<artifactId>protoncloud-sdk</artifactId>
<version>5.5.0</version>
</dependency>
The SDK is in Atomic's public Maven repository, with no credentials required. Declare the repository in
pom.xml:
<repositories>
<repository>
<id>protoncloud</id>
<url>https://maven.protoncloud.com.br</url>
</repository>
</repositories>
The versions are also still published to Atomic's GitHub Packages, which requires credentials, for the projects that still point there.
Coming from proton-lib
Up to the 5.0.0-beta versions, the SDK was called br.com.atomicsolutions.proton:proton-lib. Change only the
dependency's groupId and artifactId: the Java packages haven't changed, so no import changes.
Configuration
The address and the token are resolved in this order, from highest to lowest priority:
- System property:
-Dproton.host=...and-Dproton.token=.... - Environment variable:
PROTON_HOSTandPROTON_TOKEN. config.propertieson the classpath.
Through the runner, PROTON_HOST arrives ready to use and the run id comes in -DidDatasetRun. The updated
runner also delivers the run token in PROTON_TOKEN, and SDK 5.2 uses it even
if config.properties has a different token. As long as the file still has a token, the SDK warns once in
the run output.
With an older runner, the token comes from the PROTON_TOKEN variable on the runner machine or from
config.properties, which also lets you run outside the runner, on the developer's machine:
hostname=https://app.protoncloud.com.br/api
token=<token>
The token can come with or without the Bearer prefix. Keep the file that holds the token out of version
control.
To point the project to another server and ignore what the runner injects, declare
proton.config.precedence = file in config.properties. It applies to the address and to the personal
token, but not to the run token. Without it, when the injected value differs from the file, the SDK logs a
line saying which one won (the token is never printed).
The lifecycle of a run
The runner calls mvn -B test -DidDatasetRun=<id>, with -Dtest=TestProtonScript when the project has that
class. Since 5.3, this test only hands the component executor the
function that finds the component by name:
import br.com.atomicsolutions.proton.ProtonComponentRunner;
import org.junit.jupiter.api.Test;
// The only class in the project that imports the SDK
class TestProtonScript {
@Test
void executar() throws Exception {
ProtonComponentRunner.run(TestProtonScript::passo);
}
/** The component's step, or null when the component doesn't belong to this project. */
private static ProtonComponentRunner.Step passo(String nome) {
Componente componente = Componentes.localizar(nome);
return componente == null ? null : componente::executar;
}
}
The component receives the step parameters, already resolved by Proton (variable, output reference, value typed when the run was triggered, encrypted), and returns the outputs:
public class CriarPedido implements Componente {
@Override
public Map<String, String> executar(Map<String, String> parametros) {
String numero = new TelaDoPedido().criar(parametros.get("in_cliente"));
return Map.of("out_numero_pedido", numero);
}
}
To fail, the component throws an exception. The executor records the original stack trace (including from
AssertionError and NoClassDefFoundError) in the error panel and marks the run as Failed, with the
failure reason. Since 5.5.0, the panel shows the error, its causes and the suppressed
exceptions without the JUnit and Surefire lines and without the terminal colors.
With options:
ProtonComponentRunner.of(TestProtonScript::passo)
.evidenceDir(Path.of("prints"))
.maxSteps(100)
.closeAtEnd(sessao)
.execute();
Outside a Proton run (without -DidDatasetRun), the executor throws ProtonRunException instead of calling
the server. ProtonEnv.isProtonExecution() tells whether the process is running in a Proton run.
Since 5.3, the SDK no longer ships slf4j-simple: if you use SLF4J, declare the logging provider in your own
pom.xml. The Maven output is still uploaded as the run log.
Evidence folder
At each step, the executor uploads whatever appeared in the evidence folder since the last upload: images go to the step, everything else to the run. Whatever the project saves between two steps is uploaded with the next step.
The runner can handle several runs at the same time, in the same project folder. Since 5.5.0, each run
saves to its own folder, ProtonComponentRunner.runEvidenceDir(): evidencias/<run id>, or
evidencias outside Proton.
Path print = ProtonComponentRunner.runEvidenceDir().resolve("pedido.png");
Without evidenceDir(...), the executor looks at all of evidencias except the subfolders of other runs: what
the run saves in its own folder is uploaded only for that run, and what the project saves directly in
evidencias is still uploaded, as before.
Close at the end
The video and the trace of a session are only ready when the session closes, after the last step. With
closeAtEnd(...) (5.5.0), the executor closes the resources, in the given order and only once, and uploads
whatever appeared in the evidence folder before recording the status. This way, the video of a run that
passed also reaches Proton:
try (WebSession web = WebSession.start()) {
ProtonComponentRunner.of(nome -> Passos.localizar(nome, web))
.closeAtEnd(web)
.execute();
}
The executor also closes them when a step fails (before Failed) and when the next step belongs to another
project. An error while closing doesn't change the result: if everything passed, it becomes a warning in the
log; if a step failed, it is added as a suppressed exception to that step's error. The try-with-resources
closes the resource again, so its close() must accept being called twice.
Failure before the first step
A failure that happens before the executor starts, such as a session that doesn't open or a device that is
offline, reaches Proton through ProtonComponentRunner.recordFailure (5.5.0): the error goes to the error
panel and Failed is recorded with the error's reason, or with the reason you give.
WebSession web;
try {
web = WebSession.start();
} catch (RuntimeException e) {
ProtonComponentRunner.recordFailure(e, FailureReason.ENVIRONMENT);
throw e;
}
Outside Proton, or when the run has already finished, recordFailure records nothing.
Without the executor
Projects older than 5.3 go through the steps with the SDK classes. A simplified example:
import br.com.atomicsolutions.proton.ProtonAutomation;
import br.com.atomicsolutions.proton.ProtonLogs;
import br.com.atomicsolutions.proton.RunStatus;
import org.junit.jupiter.api.Test;
class TestProtonScript {
@Test
void runProtonExecution() {
ProtonAutomation.updateRunStatus(RunStatus.RUNNING);
try {
String nome;
while ((nome = ProtonAutomation.getCurrentComponentName()) != null && !nome.isEmpty()) {
ProtonAutomation.startComponent(); // opens the step and reads the run environment
Componentes.executar(nome); // the component code
ProtonAutomation.endComponent(); // closes the step as Passed
}
ProtonAutomation.updateRunStatus(RunStatus.PASSED);
} catch (RuntimeException e) {
ProtonLogs.setLog(e);
ProtonLogs.setErrorLog(e.getMessage());
ProtonAutomation.updateRunStatus(RunStatus.FAILED);
throw e;
}
}
}
Inside a component, without the executor
import br.com.atomicsolutions.proton.ProtonEnvironment;
import br.com.atomicsolutions.proton.ProtonFilesAndResources;
import br.com.atomicsolutions.proton.ProtonParameter;
import java.nio.file.Path;
import static br.com.atomicsolutions.proton.ProtonLogs.setLog;
public static void criarPedido() {
String cliente = ProtonParameter.getProtonValue("CLIENTE"); // dataset parameter
String url = ProtonEnvironment.get("SISTEMA_URL"); // run environment variable
setLog("Creating order for " + cliente);
// ... automation ...
ProtonFilesAndResources.uploadImage(Path.of("evidencias/pedido.png")); // step evidence
ProtonParameter.setProtonValue("NUMERO_PEDIDO", "4500012345"); // output parameter
}
Run log
Since 5.4.0, each log line has a level: Info (ProtonLogs.INFO, the default), Warning (WARNING), Error
(ERROR) or Critical (CRITICAL). The run screen filters by level and highlights warnings and errors, and
log search finds the line across runs.
ProtonLogs.logInfo("Opening transaction BP");
ProtonLogs.logWarning("Record already exists: moving on to the next one", Map.of("cnpj", cnpj));
ProtonLogs.logError("Timeout in LoginSAP", Map.of("tentativa", 3));
ProtonLogs.setLog("Message with the chosen level", ProtonLogs.CRITICAL);
- The context is an optional
Mapthat goes along with the line (the item being processed, the attempt, the return code). Environment values are masked in it too. setLog(exceção)sends the entire stack trace as an Error line.- Each line also goes to the Maven output, with the time and the level. The runner uploads this output as the run's log file: above the Proton log limit (10,000 lines), that file is where the full log is kept.
Correlation ID
Each run has a Correlation ID, the same one the Proton screen shows. The runner delivers it in
PROTON_CORRELATION_ID (runner 5.0.8 onward), and the SDK sends it in every call to Proton
(X-Correlation-Id and traceparent, in the W3C Trace Context format). To carry it into the calls the
automation makes (SAP, APIs, portals):
HttpRequest.Builder pedido = HttpRequest.newBuilder(uri);
ProtonEnv.correlationHeaders().forEach(pedido::header);
Outside Proton, or with a runner that doesn't deliver the id yet, correlationHeaders() returns an empty map.
Run status
Proton 5 accepts RunStatus.RUNNING, PASSED and FAILED. Since 5.4.0, FAILED can carry the reason,
which says why the run failed without changing the status:
ProtonAutomation.updateRunStatus(RunStatus.FAILED, FailureReason.BUSINESS);
| Reason | When |
|---|---|
FailureReason.BUSINESS | A business rule didn't allow the run to continue: a record that already exists, an invoice already rejected. Running again gives the same result. |
FailureReason.TECHNICAL | Any other error: an element that didn't show up, a system that is down, a code exception. This is the default. |
FailureReason.ENVIRONMENT | The environment didn't allow the run: a missing dependency, a wrong setting. |
With the component executor, just throw the reason's exception in the step:
if (cadastroExiste) {
throw new ProtonBusinessError("CNPJ " + cnpj + " is already registered");
}
The executor looks for the reason in the error and in its causes (FailureReason.of(erro), 5.5.0), and the
first one that states a reason wins:
ProtonBusinessError: business failure;ProtonEnvironmentError(5.5.0) andProtonEnvironmentException(the run environment without the requested variable): environment failure;- an exception that implements
ProtonFailure(5.5.0): the reason it states; - any other error: technical failure.
ProtonFailure is for an exception that already extends another class, such as one from a library, and
therefore can't extend ProtonBusinessError or ProtonEnvironmentError:
public class SessaoNaoAbriu extends RuntimeException implements ProtonFailure {
@Override
public String failureReason() {
return FailureReason.ENVIRONMENT;
}
}
The Proton 4 constants (FAILED_DATA, FAILED_ENVIRONMENT and IN_PROCESS) are deprecated: the server
rejects them, and the SDK sends FAILED in place of the first two, with the business and environment
reasons, and RUNNING in place of the last one, with a warning in the run output.
BrowserStack
In a run on BrowserStack, the Proton trigger chooses the device, the OS version and the app.
ProtonBrowserStack provides them for opening the session and records the BrowserStack session id in the
run, so you can find the run in the BrowserStack dashboard:
| Method | What it does |
|---|---|
getBStackDevice() | Device name (Samsung Galaxy S23) |
getBStackDeviceOsVersion() | OS version chosen when the run was triggered (13.0); empty with a Proton that doesn't return it yet (5.5.0) |
getBStackAppReference() | App on BrowserStack (bs://...) |
recordBSSessionId(id) | Records the session id in the run and returns the HTTP status: 200 means it was recorded (5.5.0) |
int status = ProtonBrowserStack.recordBSSessionId(driver.getSessionId().toString());
if (status != 200) {
ProtonLogs.logWarning("Proton didn't record the BrowserStack session", Map.of("status", status));
}
updateBSSessionId(id) still works, deprecated, without returning the status: when Proton doesn't record
it, it writes a [proton] line to the output. The recording uses the
PUT /dataset-run/{id} route, and the OS version comes
from deviceOsVersion in GET /dataset-run/{id}.
Reference
| Class | Method | What it does |
|---|---|---|
ProtonComponentRunner | run(resolver) and of(resolver) | Runs the dataset steps with the component executor |
closeAtEnd(recursos...) | Closes the resources after the last step, before the status (5.5.0) | |
runEvidenceDir() | The run's evidence folder (5.5.0) | |
recordFailure(erro) and recordFailure(erro, motivo) | Records a failure that happened before the executor (5.5.0) | |
ProtonAutomation | startComponent() | Opens the current step and reads the run environment |
endComponent() | Closes the current step | |
getCurrentComponentName() | Name of the current component; empty when there are no more steps | |
getCurrentComponentSystem() | System of the current component | |
updateRunStatus(status) and updateRunStatus(status, motivo) | Marks the run with a RunStatus and, for FAILED, the reason | |
getDatasetRunInfo() | Run data | |
ProtonParameter | getProtonValue(nome) | Value of a dataset parameter in the open step |
getProtonAllComponentParameters() | All parameters of the open step | |
setProtonValue(nome, valor) | Records an output parameter | |
getProtonOutputParameterList(nome) | Outputs recorded in the run with that name | |
ProtonLogs | setLog(texto) and setLog(texto, nível) | Writes to the run log, with the level |
logInfo, logWarning, logError and logCritical | Writes with the level in the name and an optional context | |
setLog(texto, true) | Writes in Markdown, without date and time | |
setLog(exceção) | Writes the stack trace to the log, as Error | |
setErrorLog(texto) | Records the run error | |
ProtonFilesAndResources | uploadImage(Path) | Uploads an image as evidence |
uploadVideo(Path) and uploadFileResource(Path) | Uploads a video or a file as evidence | |
ProtonEnvironment | get(nome) and others | Run environment: see Run environment |
ProtonEnv | getHost(), getToken() and getIDDatasetRun() | Process configuration |
isProtonExecution() | Whether the process is running in a Proton run (5.5.0) | |
getCorrelationId() and correlationHeaders() | The run's Correlation ID and the headers to pass it along | |
FailureReason | BUSINESS, TECHNICAL and ENVIRONMENT | Failure reason |
of(erro) | Reason of an error, also looking at its causes (5.5.0) | |
ProtonBrowserStack | getBStackDevice(), getBStackDeviceOsVersion() and getBStackAppReference() | Device, OS version and app chosen when the run was triggered |
recordBSSessionId(id) | Records the BrowserStack session id in the run (5.5.0) |
Communication errors
A server error doesn't stop the automation: reads come back empty (getProtonValue also returns "" when
the parameter doesn't exist), and a failed output write is logged in the run output. When a value comes back
empty for no apparent reason, start with the log.
The exception is the run environment: reading a variable that doesn't exist throws
ProtonEnvironmentException, because continuing with an empty value would hide the problem. Since 5.5.0, in
the component executor, it counts as an environment failure.