Skip to main content
Proton v5

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:

  1. System property: -Dproton.host=... and -Dproton.token=....
  2. Environment variable: PROTON_HOST and PROTON_TOKEN.
  3. config.properties on 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 Map that 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);
ReasonWhen
FailureReason.BUSINESSA 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.TECHNICALAny other error: an element that didn't show up, a system that is down, a code exception. This is the default.
FailureReason.ENVIRONMENTThe 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) and ProtonEnvironmentException (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:

MethodWhat 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​

ClassMethodWhat it does
ProtonComponentRunnerrun(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)
ProtonAutomationstartComponent()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
ProtonParametergetProtonValue(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
ProtonLogssetLog(texto) and setLog(texto, nível)Writes to the run log, with the level
logInfo, logWarning, logError and logCriticalWrites 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
ProtonFilesAndResourcesuploadImage(Path)Uploads an image as evidence
uploadVideo(Path) and uploadFileResource(Path)Uploads a video or a file as evidence
ProtonEnvironmentget(nome) and othersRun environment: see Run environment
ProtonEnvgetHost(), 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
FailureReasonBUSINESS, TECHNICAL and ENVIRONMENTFailure reason
of(erro)Reason of an error, also looking at its causes (5.5.0)
ProtonBrowserStackgetBStackDevice(), 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.