Speedway has moved into Loadster Site & API Monitoring.
If you have an existing Speedway account, you can use the same credentials to sign in to Loadster. Don't worry, your monitors haven't missed a beat and your monitoring data is intact. Everything that Speedway did, Loadster now does the same or better.
We're leaving the Speedway documentation up for the time being, but strongly encourage you to refer to the Site & API Monitoring section of the Loadster manual.
So far, we’ve mostly been working with steps that perform a single action at a time, like HTTP steps in Protocol Scripts or navigate, click, and type steps in Browser Scripts.
Those single steps can accomplish a lot if your script proceeds in a linear fashion, as most test scripts should… but what if you need special control flow or conditional logic or looping?
Speedway’s answer is Code Blocks.
Code blocks can exist anywhere in your script. In fact, if you wanted to you could make a script that’s just a single code block, and do everything in JavaScript.
Code blocks are more flexible than ordinary step-by-step scripting because you have the control flow of an actual programming language: looping, conditionals, functions, etc.
To add a code block to your script, select Add Code Block from the top bar.
Code blocks are individually scoped to the bot that is executing them. That means that if you declare a variable or function in a code block, it will exist only for that bot running the script, and not for any other bots that might also be running the script.
Just like a real human user, each bot interacts with your site independently of all the others.
Also, since the scripting language is JavaScript, ordinary JavaScript variables (declared with let
or const
or
var
) within a code block might be undefined outside that code block.
If you need a variable to persist between steps, set it as a special bot variable with bot.setVariable("v", v)
so
that the value ${v}
remains in scope throughout the script.
Beyond all the standard JavaScript language constructs, code blocks expose a few important objects specific to Speedway that you can use in your scripts.
The bot
object is global within the context of a single bot. It represents the bot currently executing the script.
This bot
object exposes the following methods:
/**
* Pause for a specific number of milliseconds (these are synonymous).
*/
bot.wait(milliseconds);
bot.sleep(milliseconds);
/**
* Get the time, in milliseconds, relative to the start of the test.
*/
bot.getTime();
/**
* Get and set bot variables.
*/
bot.getVariable('account'); // get the value of the "account" bot variable
bot.getVariable('account', 1); // get the second column, if it's a multi-column dataset
bot.setVariable('user', 'a1350'); // set a bot variable
/**
* Override the global halt setting, just for this bot.
* This will allow HTTP 4xx/5xx and other things that would normally stop the script, without stopping the script.
*/
bot.setHaltOnErrors(false);
/**
* Tells how many iterations of the script have been completed by this bot, starting with 0.
*/
bot.getIteration();
/**
* Identifies which bot is currently running the script.
* This can be useful in special cases when only certain bots should perform certain actions.
*/
bot.getBotNumber(); // index of the bot in the group, starting with 0
bot.getBotGroupNumber(); // index of the bot group, starting with 0
bot.getBotIdentifier(); // unique bot identifier string with a guid
/**
* Manually requests a trace that will show up in the Traces section of the report.
*/
bot.trace();
/**
* Starts and stops a timer. When you use custom timers, the amount of time elapsed between starting and
* stopping the timer will be reported in the results.
*/
bot.startTimer("My Checkout Flow");
bot.stopTimer("My Checkout Flow");
/**
* Runs a custom function inside a timer. This is shorthand for starting and stopping the timer separately.
*/
bot.timer("My Checkout Flow", () => {
});
/**
* Reports a custom transaction timing directly, in milliseconds. Useful if you want to calculate it yourself.
*/
bot.timer("My Checkout Flow", 3150);
/**
* Makes the bot exit the script early.
*/
bot.exit();
It’s important to note that, even though the syntax is JavaScript, these bot methods are all synchronous.
There’s no need to do a promise chain or callbacks await
or anything like that, because the actual processing is done
behind the scenes by multi-threaded workers, so synchronous programming here is not the dangerous practice it might
seem to be if you come from a background in single-threaded JavaScript development.
Every bot has access to an HTTP client called http
that can make requests. These equate to regular HTTP steps in an
HTTP script, but you can run them programmatically from a code block too.
// Make HTTP requests
http.get(url, args);
http.post(url, body, args);
http.put(url, body, args);
http.patch(url, body, args);
http.delete(url, args);
http.options(url, args);
http.trace(url, args);
// Add a header to all future requests to a host
http.addHostHeader('petstore.loadster.app', 'Authorization', 'Bearer 5a830a310b5ca38a');
// Remove host headers matching a host/name/value
http.removeHostHeaders('petstore.loadster.app');
http.removeHostHeaders('petstore.loadster.app', 'Authorization');
http.removeHostHeaders('petstore.loadster.app', 'Authorization', 'Bearer 5a830a310b5ca38a');
// Add or remove global headers for all requests to all hosts
http.addGlobalHeader('X-Automated-Testing', 'yes');
http.removeGlobalHeader('X-Automated-Testing');
Later in the manual you can find examples of making HTTP requests in a code block, and what to pass for the args
.
In browser scripts, code blocks also give you direct programmable access to the bot’s browser
instance via the browser
object.
Here are a few examples of basic browser scripting in a code block, very similar to what you can do with individual steps.
// Navigate to a page and click on a button
browser.navigate('https://petstore.loadster.app');
browser.click('button.login-button');
// Wait 3 seconds
bot.wait(3000);
// Wait for an element to be "visible", "hidden", "attached", or "detached"
browser.waitFor('.spinning-overlay', 'hidden');
// Wait up to 5 seconds for an element to be hidden, and then silently move on
browser.waitFor('.spinning-overlay', 'hidden', { timeout: 5000, silent: true });
// Wait for a certain URL to be loaded in the browser's URL bar
browser.waitForUrl('https://example.com/example');
// Wait for a certain URL to be loaded, but silently move on if it doesn't
browser.waitForUrl('https://example.com/example', { timeout: 5000, silent: true });
// Type a username and password and submit the form
browser.type('.username', 'sloth');
browser.type('.password', 'chunk');
// Type instantly with no delay between keystrokes
browser.type('.greeting', 'Hello', { delay: 0 });
// Type very slowly with 1250ms between keystrokes
browser.type('.greeting', 'Hello', { delay: 1250 });
// Press individual keys on the keyboard (for testing games, etc)
browser.keyboard.press('ArrowUp');
browser.keyboard.press('X');
browser.keyboard.press('Enter');
// Click the submit button on a form
browser.click('form input[type=submit]');
// Click something if it shows up within 3 seconds, otherwise move on without complaining
browser.click('#cookie-warning', { timeout: 3000, silent: true });
// Take a screenshot
browser.screenshot();
// Choose a file in a file input
browser.chooseFiles('input[type=file]', [{ name: 'lolcat', contentType: 'image/jpeg', contentBase64: 'UklGRuZ/AABXRUJQVlA4INp/AABwrAGdASr0AQACPjkYi0QiIaET' }]);
// Hover a menu and click the first item
browser.hover('.menu');
browser.click('.menu li:first-child');
// Select an option from a select element
browser.selectByIndex('select[name=countries]', 1);
browser.selectByValue('select[name=countries]', 'HR');
browser.selectByText('select[name=countries]', 'Croatia');
These are blocking synchronous calls with processing happening behind the scenes, so you don’t need to use
await
or callbacks or promise chains for sequential browser actions.
Code blocks run outside the browser, so they don’t have access to JavaScript state inside the bot’s browser, such as DOM elements and variables on the page.
To access the from your code block, you can use eval
, which is like an
evaluate block but embedded in a code block.
Here’s an example of control flow in a code block that checks how many .invoice
elements exist in the browser.
If it’s more than 0, it archives one. Otherwise it logs a message and does nothing.
let invoiceCount = browser.eval("document.querySelectorAll('.invoice').length");
if (invoiceCount > 0) {
browser.click('.invoice .archive-button');
} else {
console.log('There are no more invoices!');
}
Keep in mind that the control flow is happening in the code block outside the browser. Only the JavaScript passed
to browser.eval()
happens inside the browser.
Each browser bot has its own incognito browser window to start with, and you can open additional windows/tabs in your script. Your site might automatically open other windows/tabs, too.
Here are a few ways you can open, close, resize, and switch between windows in a browser script.
// Resize the bot's browser viewport
browser.setViewportSize(1800, 1200);
// List all of the bot's browser windows/tabs
browser.listWindows();
// Opens a new window/tab and makes it active
browser.openWindow();
// Get the bot's currently active browser window/tab
browser.getActiveWindow(); // 0, 1, 2...
// Focus a different browser window/tab
browser.setActiveWindow(0); // the bot's original tab
browser.setActiveWindow(1); // the first opened tab
browser.setActiveWindow(2); // and so on...
// Close a browser window/tab
browser.closeWindow(1);
By default, browser bots do the polite thing and identify as headless browsers via the User-Agent
header.
Your site’s analytics scripts might rely on this header to exclude bot traffic, so it doesn’t mess up your analytics data. Occasionally, sites block headless browsers altogether, since it often means bot or scraper traffic.
If you want to override the User-Agent
header to bypass your site’s own rules or get special treatment, you can
do so, but only at the beginning of your script, before performing any browser actions.
// Set a custom User-Agent (window.navigator.userAgent + all request headers)
browser.setUserAgent('Loadster/Speedway');
// Or pretend to be a normal, non-headless browser
browser.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36');
Setting a custom user agent will affect all outgoing requests, as well as the window.navigator.userAgent
string when evaluated on a web page.
You might need the bot to send a custom request header with every request.
Sometimes this is necessary to bypass your site’s bot filtering or DDoS protection to allow test traffic.
You might need to override the User-Agent
header for similar reasons.
// Set a global request header to send to all domains
browser.setGlobalHeader('X-Loadster', 'true');
// Remove a global request header
browser.removeGlobalHeader('X-Loadster');
// Set a request header for all requests to a specific domain
browser.setHostHeader('example.com', 'X-Loadster', 'true');
// Remove a request header for a specific domain
browser.removeHostHeader('example.com', 'X-Loadster');
// Set HTTP Basic or NTLM credentials for any site that requires them
browser.setHttpAuthentication('myUsername', 'myPassword');
You can set and unset global headers at any point in your script with a code block. Once you set a global header, the bot will send it with all subsequent requests, until you remove it or the end of the script is reached.
Setting custom headers in the browser may have the side effect of disabling some or all of the browser’s cache.
Browser Bots run real web browsers, so normally they automatically load all resources on your page just like a normal browser does.
If you need to block a certain resource or all resources matching a substring, you can do so in a code block.
browser.blockRequestsIfUrlContains('analytics.js');
browser.blockRequestsIfUrlContains('.jpg');
browser.blockRequestsIfUrlContains('cdn.example.com');
This code block must occur in your script before the bot loads those resources in order to stop them from loading. Keep in mind that if you block an essential script, your page might not load or function correctly.
Blocking browser resources in this way may have the side effect of disabling some or all of the browser’s cache.
In most cases, bots handle cookies automatically and you don’t have to do anything.
But sometimes, you might need to set custom cookies in your test script to get around authentication or bypass a CAPTCHA or something like that.
// Add a cookie for a specific base URL
browser.addCookie({ url: 'https://example.com', name: 'automated-testing', value: 'true' });
// Add a cookie for a domain and path
browser.addCookie({ domain: 'example.com', path: '/', name: 'automated-testing', value: 'true' });
// Add a cookie with an expiration and security attributes
browser.addCookie({
url: 'https://example.com',
name: 'automated-testing',
value: 'true',
expires: 86400,
httpOnly: true,
secure: true,
sameSite: true
});
Cookies that you add with this method will automatically persist for the duration of the browser session, and can coexist with other cookies that the browser might be receiving through ordinary means.
If your site requires HTTP Basic or NTLM authentication, you can supply credentials in a code block.
// Set HTTP Basic or NTLM credentials for any site that requires them
browser.setHttpAuthentication('myUsername', 'myPassword');
Once you set username and password credentials in your script, the bot will send them to any site that asks for them, so make sure you don’t test any malicious sites that might steal them.
Keep in mind, this approach only works for HTTP protocol authentication, not the more common form-based authentication used in web applications today.
Browsers natively support confirm and alert to get user input. These are a bit antiquated, and many applications now use custom modal dialogs on the page instead. Still, Speedway bots know how to respond to these confirmation popups.
By default, bots always answer in the negative by clicking Cancel on a confirm message, and they simply close an alert message. If you need to answer a confirmation in the affirmative (clicking OK) you can tell the bot ahead of time.
// Answer confirm() dialogs with "OK" instead of "Cancel"
browser.setDialogConfirmation(true);
// Answer confirm() dialogs with "Cancel" instead of "OK"
browser.setDialogConfirmation(false);
If you pass true
here, the bot will answer OK or Yes to subsequent confirmation prompts. You can revert to
the original less agreeable behavior by passing false
.
Web browsers may allow JavaScript on the page to query the browser’s geographical location through the Geolocation API. Typically this is only supported for pages on which the user allows it.
Bots have location services enabled by default, with each bot assigned a random latitude and longitude somewhere on Earth. This is often sufficient for testing most sites with mapping or location features.
If you need to set a specific latitude and longitude in your script you can do so. You can also disable location permissions altogether.
// Enable geolocation at a specific latitude and longitude
browser.enableGeolocation(40.7128, -74.0060);
// Disable geolocation
browser.disableGeolocation();
Setting or disabling the browser location needs to happen at the beginning of your script, before any browser actions, to have any effect.
Code blocks also expose a simple console
for logging:
console.log(message);
console.warn(message);
console.error(message);
Messages written to the console show up in the script logs and assist you in debugging the script.
If you’re testing APIs, you’ll often need to parse JSON data to look at specific properties. You can use the ordinary
JSON.parse
for this.
var simple = JSON.parse("{a: 1}"); // parse an arbitrary JSON string
var body = JSON.parse(response.string()); // parse the response body in a validator
Since XML parsing isn’t a standard language feature of JavaScript, Speedway includes the open source xmldoc parser. Additional documentation for this parser is available on GitHub, but here’s a quick example:
let xml = XML.parse(response.string());
let users = xml.childrenNamed("user");
Speedway provides a built-in formats
library to help you with encoding and decoding strings, and with generating
timestamps, UUIDs, and random data. Here are some examples of inputs and outputs.
formats.uppercase("hello"); // HELLO
formats.lowercase("HELLO"); // hello
formats.urlencode("user@example.com"); // user%40example.com
formats.urldecode("user%40example.com"); // user@example.com
formats.base64encode("user:pass"); // dXNlcjpwYXNz
formats.base64decode("dXNlcjpwYXNz"); // user:pass
formats.xmlescape("11 > 10"); // 11 > 10
formats.xmlunescape("11 > 10"); // 11 > 10
formats.htmlescape("<p>"); // <p>
formats.htmlunescape("<p>"); // <p>
formats.timestamp("%Y-%M-%d"); // 2020-07-04
formats.randomalpha(12); // zmLkWMwtEhOD
formats.randomalphanumeric(10); // F6kEq53p3W
formats.randomnumeric(8); // 62331478
formats.uuid(); // 8ffdb4ef-9e87-4b58-9415-f4c454f0a2ee
Occasionally you might need to apply a hashing function to a piece of data (with an optional secret)
to generate a hashed value that your server expects. Some of the more commonly used hashes are included
in Speedway’s built-in crypto
library, which you can call from any code block.
crypto.hmacsha512base64("input", "secret"); // Ksle03F+BCxwZKX6fDGCMM022F4G+P+Dc9BMoX42Fingn0a38VH/OCo/SMWxkSFEbkXCWI8P8d6fdLBADa74Hw==
crypto.hmacsha384base64("input", "secret"); // is0XLFfl9kpaMnpDdiMkwOJ4eYP7ez481SOKgiu6p/mC4SXCJzeVtbuU0z6auD7F
crypto.hmacsha256base64("input", "secret"); // jYmF0Et6vTLLqjd5o9qgGeDSaaIq7BWvjnKW9wLMaMY=
crypto.hmacsha224base64("input", "secret"); // LDdw8G3Ykt8vLV9+8gXABot+TCB01il0Hy5S8A==
crypto.hmacsha1base64("input", "secret"); // MEQPNt3CgJu9TIsfN6boDXWIwwM=
crypto.hmacsha512hex("input", "secret"); // 2ac95ed3717e042c7064a5fa7c318230cd36d85e06f8ff8373d04ca17e361629e09f46b7f151ff382a3f48c5b19121446e45c2588f0ff1de9f74b0400daef81f
crypto.hmacsha384hex("input", "secret"); // 8acd172c57e5f64a5a327a43762324c0e2787983fb7b3e3cd5238a822bbaa7f982e125c2273795b5bb94d33e9ab83ec5
crypto.hmacsha256hex("input", "secret"); // 8d8985d04b7abd32cbaa3779a3daa019e0d269a22aec15af8e7296f702cc68c6
crypto.hmacsha224hex("input", "secret"); // 2c3770f06dd892df2f2d5f7ef205c0068b7e4c2074d629741f2e52f0
crypto.hmacsha1hex("input", "secret"); // 30440f36ddc2809bbd4c8b1f37a6e80d7588c303
crypto.md5("input"); // a43c1b0aa53a0c908810c06ab1ff3967
crypto.sha1("input"); // 140f86aae51ab9e1cda9b4254fe98a74eb54c1a1
crypto.sha256("input"); // c96c6d5be8d08a12e7b5cdc1b207fa6b2430974c86803d8891675e76fd992c20
crypto.sha512("input"); // dc6d6c30f2be9c976d6318c9a534d85e9a1c3f3608321a04b4678ef408124d45d7164f3e562e68c6c0b6c077340a785824017032fddfa924f4cf400e6cbb6adc
Don’t get too excited by the word “crypto” – the object is called that because it’s a container for cryptography and hashing functions, not necessarily related to cryptocurrencies.
There’s nothing like learning by example! Here are a few contrived examples of things you can do with code blocks.
The http
object (representing the HTTP user agent belonging to the currently running bot) has methods for all of the
common HTTP methods (GET, POST, etc).
// GET
http.get("https://petstore.loadster.app");
// GET with additional page resources
http.get("https://petstore.loadster.app", {
resources: [
"/style.css",
"/favicon.ico"
]
});
// POST with a JSON body
http.post(
"https://petstore.loadster.app/api/bag",
{
id: 16,
name: 'Pocket Rex'
},
{
headers: {
"Content-Type": "application/json"
}
}
);
// POST with a form body
http.post(
"https://petstore.loadster.app/api/login",
"username=sloth&password=chunk",
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
// DELETE
http.delete("https://petstore.loadster.app/bag/16");
You can pass custom request headers with each request, either as an object with key-value pairs, or in an array.
// Pass request headers in an object
http.get("https://petstore.loadster.app/api/bag", {
headers: {
"Accept": "application/json"
}
});
// Pass request headers in an array
http.get("https://petstore.loadster.app/api/bag", {
headers: [
{ name: "Accept", value: "application/json" }
]
});
Validators (similar to the Validation Rules that you can use with ordinary HTTP steps) call a JavaScript function to examine the response and return true if it’s valid, or false if it’s not.
Validator functions can be normal JavaScript functions or the newer ES2016+ arrow functions.
You can specify multiple validator functions for a single response.
// A POST with a JSON body and validator function that requires an HTTP 201 status
http.post(
"https://petstore.loadster.app/api/purchase",
{
// items were added to the cart previously, so no body required
},
{
headers: [
{ name: "Content-Type", value: "application/json" }
],
validators: [
// regular function validator syntax
function (response) {
return response.status == 201;
},
// arrow function validator syntax works too!
response => response.status === 201
]
}
);
Often, the server will send you some data that you need to save and use later in your script.
In code blocks, you can capture these from the response and store them using a validator function. Note that we use
the validators
for capturing too; there is no separate property
for Capturing Rules as there is with ordinary HTTP steps.
Simply call bot.setVariable(name, value)
anywhere in your code block to set a bot-scoped variable. These special
variables are available for the bot’s entire iteration of the script, unlike ordinary JavaScript variables
which are scoped to the individual code block and may not be available to subsequent steps.
// Create a random numeric string between 0-999999
let random = String(Math.floor(1000000 * Math.random()))
// POST to register an account with the random username and capture a user token
http.post(
"https://petstore.loadster.app/api/register",
{
username: "sloth_" + random,
password: "chunk"
},
{
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
validators: [
response => {
var body = response.json();
if (body.token) {
bot.setVariable("token", body.token); // save the auth token for later
return true; // valid response! treat as a success
} else {
return false; // invalid response! treat as a failure
}
}
]
}
);
By default, Speedway automatically interprets any HTTP 400 or higher status code as an error, and reports it as such.
But there are times when you might actually want an HTTP 4xx or 5xx status code. For example, you might be testing a REST API and expect it to return HTTP 409 Conflict when the resource already exists.
In such cases, you can use ignoreHttpErrors: true
with your request, so that Speedway will ignore the HTTP
status codes and you can interpret the status code yourself.
// Make sure we get HTTP 409 when we try to register a duplicate user
http.post(
"https://petstore.loadster.app/api/register",
{
username: "admin",
password: "admin"
},
{
ignoreHttpErrors: true,
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
validators: [
response => {
return response.status === 409;
}
]
}
);
Base64 encoding is often used to encode non-string data as an ASCII string, so it can be sent through
text-based protocols or compared in a text editor. In code blocks, we expose the formats
library to handle Base64
encoding and decoding.
const originalBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAZAAAACQCAIAAAA=';
const byteArray = formats.base64decode(originalBase64); // returns a Uint8Array
const encodedBase64 = formats.base64encode(byteArray);
if (encodedBase64 === originalBase64) {
console.log('Good! Re-encoding the byte array got the same Base64 string we started with.');
}
If the Base64 encoded data is actually a string, you’ll need to convert the byte array into a string afterwards
using String.fromCharCode
, like this:
const decodedByteArray = formats.base64decode('dXNlcjpwYXNz');
const decodedString = String.fromCharCode.apply(null, decodedByteArray);
const [username, password] = decodedString.split(/:/, 2);
console.log(username);
console.log(password);
Looping and conditionals can be done with all the usual JavaScript language constructs. This trivial example shows
looping with a for
loop, the %
modulus function, an if
statement, and setting and getting special bot
variables with variable interpolation of ${random}
.
console.log("We're in a nonsensical code step.");
console.log("It exists only to demonstrates random numbers, bot vars, and conditionals.");
// Set a bot variable to a random integer from 0 to 19
bot.setVariable("random", Math.floor(Math.random() * 20));
// Loop and make 20 requests, with a random value and random wait times
for (var i = 0; i < 20; i++) {
if (i % 2 == 0) {
http.get("https://example.com/evens/${random}");
} else {
http.get("https://example.com/odds/${random}");
}
http.wait(Math.random() * 2000);
}
Keep in mind that code blocks do not run in the browser, so they don’t have direct access to the live DOM
the way browser-based JavaScript does. Be careful not to confuse them with on-page JavaScript (jQuery, React, etc).
Rather, the scripts are executed by your bots running on the load engine, and they have access to objects like
bot
, http
, response
, console
, etc.
If you need to run custom code inside the bot’s browser, check out Evaluate Blocks.
If you get stuck with code blocks or want to request another helper library, we’re happy to help. We’re always interested in how you’re using code blocks in Speedway.