Skip to main content

Intro to Session API - Part 2

· 11 min read
Jason Thompson

In Part 1, we setup the Frame Session API, wired it up to a button, created EventListeners for session-related happenings, and even told our users what as going on. So far, we've mostly avoided any type of error handling (or styling but we'll get to that shortly).

What we're going to build

Implementing error handling

So, what happens if we try to start a session, but our Frame account has no more virtual machines available? or... what happens if the user's token is expired? What happens if there's a typo in the terminalConfigId?

A simple approach to catch errors like this can be accomplished by wrapping our Session API calls with a few try / catch blocks. Catching javascript errors like this can help us report any unexpected issues to the JS console as well as populate a new div inside #events with the error details for our users to see.

Ok, let's get started catching errors! First, let's wrap most of our logic in a try/catch block. This will help catch issues when calling createInstance and bind.

try {
const terminal = await FrameTerminalApi.createInstance(frameOptions);
const clicker = document.getElementById("the-clicker");

// ... Our many Terminal Event bindings

clicker.addEventListener("click", async function (event) {
event.preventDefault();
const session = await terminal.getOpenSession();
if (session && session.state === "OPEN") {
await terminal.resume(session.id);
} else {
await terminal.start();
}
});
clicker.disabled = false;
} catch (error) {
addEventItem(`Error: ${error.message}`);
console.error(`Session Error: ${error}`);
}

Next, let's add another try/catch around our session-start logic, like so:

clicker.addEventListener("click", async function (event) {
event.preventDefault();
try {
const session = await terminal.getOpenSession();
if (session && session.state === "OPEN") {
await terminal.resume(session.id);
} else {
await terminal.start();
}
} catch (error) {
addEventItem(`Error: ${error.message}`);
console.error(`Something went wrong: ${JSON.stringify(error)}`);
}
});

Great! Should we run into anything unexpected, we can find details in the JS Console and a simple message rendered to the browser.

Handling expired tokens

If your JWTs become expired or are invalid, we can check for this and handle it accordingly, and in an ideal situation, you can gracefully handle the error and automate getting a fresh token and retrying.

Checking the JWT expiration beforehand

Ideally, you can validate your JWTs beforehand but this is optional if you have a proper authentication flows in place.

// example JWT
const jwt =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpbWcta2RzLnB1YiIsIm5iZiI6MTY1MTc2OTI5MSwiZXhwIjoxNjUxNzk4MDkxLCJqdGkiOiIxNTgyNjc3MDEiLCJzdWIiOiI5YTZmMTlmNy00YTdhLTRlNjgtOTkxMi02MzdlMGM2ZmU2MGQiLCJhdWQiOiJodHRwczovL2ZyYW1lLm51dGFuaXguY29tIiwidG9rZW5Db250ZW50IjoiMThZMjBKYVhJQ0JKNzlSOFJZODRQVEgzWUEySiJ9.q9QWHLZKoOhLa0oCqqdDvx1_eI0KwJcEOWGz_B_ys9I";

// Decode the JWT's payload from base64
const payload = JSON.parse(atob(jwt.split(".")[1]));
const expirationTime = payload.exp * 1000; // convert to milliseconds
const currentTime = Date.now();

// Check expiration.
if (currentTime >= expirationTime) {
console.log("Token expired");
} else {
console.log("Token not expired");
}

Catching a 401 response

Checking for unauthorized responses (401s) is relatively easy. We can conditionally check for a status code and log/handle the error message appropriately.

try {
terminal = await createInstance(terminalOptions);
//
} catch (error) {
if (error.networkError && error.networkError.statusCode === 401) {
console.log("Error: 401 Unauthorized. Token expired.");
addEventItem(`Error: JWT is expired or invalid.`);
} else {
console.log("Error: " + error.message);
}
}

An expired token typically happens because of a page becomes stale. If a page sits long enough, the token can expire -- then, if a user tries to start a session with a stale/expired token, they'll of course get a 401 response. Ideally, this is where you want to handle this gracefully and ideally, redirect the user somewhere that they can get a new token.

Additionally, you could expand this to check and handle other scenarios as well. For example, what happens when you have a valid JWT but try to access an account/Launchpad that it doesn't have access/permissions to? That would return a 403 Forbidden response. It would be relatively easy to check for that and show a message to the user, asking them to speak with their Administrator about permissions.

It's time to make it look better... with CSS!

That just about covers the basics regarding the Session API. However, this looks more than a bit dry and nobody would use our webpage, even if we paid them to (probably). The good news is that we've only a few HTML elements here: body, p, button, and some divs. Styling these few should be a breeze. Let's see if we can make this more interesting with the following CSS inside of a new <style> tag nested inside of our index.html's <head>:

<head>
<title>Hello, Frame!</title>
<script src="https://unpkg.com/@fra.me/terminal-factory@2.38.0/api.js"></script>
<style>
body {
margin: 0;
font-size: 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
color: #feb17d;
height: 100vh;
overflow: clip;
background: rgb(55, 82, 109);
background: linear-gradient(rgba(172, 129, 136, 1), rgba(55, 82, 109, 1));
font-family: Arial, Helvetica, sans-serif;
}

p {
margin-top: 60px;
font-size: 2rem;
letter-spacing: -1px;
font-weight: 400;
}

button#the-clicker {
background-color: #6768ab;
border: 2px rgba(255, 255, 255, 0.25) solid;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
color: white;
font-size: 1.5rem;
cursor: pointer;
transition: background-color 0.25s;
}

button#the-clicker:hover {
background-color: #7d7fd3;
border: 2px white solid;
}

button#the-clicker:disabled {
background-color: dimgrey;
border-color: darkgrey;
cursor: wait;
}

#events {
display: flex;
flex-wrap: wrap;
min-width: 100px;
margin-bottom: 60px;
width: 100%;
padding-left: 10px;
border-top: 1px rgba(214, 214, 214, 0.301) solid;
padding-top: 20px;
}

#events div:last-child {
font-weight: bold;
animation: pulse 3s ease infinite alternate;
font-style: italic;
}

#events div:last-child:after {
content: none !important;
}

#events div {
height: 40px;
line-height: 40px;
display: inline-block;
position: relative;
background-color: #0d2432;
color: white;
padding: 0 10px;
margin-left: 30px;
margin-bottom: 10px;
}

#events div:after {
color: #0d2432;
border-left: 20px solid;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
display: inline-block;
content: "";
position: absolute;
right: -20px;
top: 0;
}

@keyframes pulse {
0%,
100% {
background-color: #0d2432;
}
50% {
background-color: #37526d;
}
}
</style>
</head>

Once you've copied the above <style> tag and it's CSS into your <head>, then, save and refresh. With a little over 100 lines of CSS later, you should have something like this:

CSS that doesn&#39;t burn my retinas

And here's what resuming a session looks like now:

CSS in action!

Ok, so what'd we just do?

  • Color-wise: I'm often lazy and unless someone tells me exactly what colors to use, I'll go find something I can quickly work with. Since it was 2022 when I wrote this, I chose a sunset-style pallet based on the Pantone 2022 Color of the Year.
  • Spacing/padding/margin: I wanted to give our elements some of their own breathing room. Our initial <p> is larger with a subtle text-shadow, and our <body> has enlarged text and takes advantage of CSS's flexbox to nicely space out its child elements.
  • The clicker: This type of button is often called the “call to action” - I made it pop out, front-and-center, and gave it a little flair when hovered over. Additionally, I added some style for the “disabled” state of the button to help people know “AYYY-WE'RE WORKIN' OVA HERE!”, but without whatever accent you're hearing right now.
  • Session events: I wanted to give them their own, separate section. And with a little additional CSS, we can make these events read left-to-right in the order they're received, along with a small “pulsing” animation for the last event as it's happening. Since we don't have any kind of loading spinner or progress bar in this exercise, this helps users know what's going on (via our event descriptions) and what the currently active step is.

Taking it further

While this is basic and serves a purpose to “get your feet wet”, there's a lot more we could do to improve on the UX and functionality of our Session API integration(s). I'll list off a few ideas:

  • Log important events and errors to a 3rd party service (Sentry, Logrocket, Loggly, Splunk, etc.).
  • Use Frame's Session API as a NPM module to take advantage of modern JS bundling/building capabilities (Vite, Rollup, Parcel, Webpack, etc.).
  • Rewrite our page in Svelte, React, Vue, or Angular (if you're into those kind of things like I am).
  • Convert this project to Typescript (seriously, it's awesome and worth the effort).
  • Setup a Secure Anonymous Token provider and dynamically generate auth tokens on-the-fly via API (this is the way).
  • Convert your page to an installable PWA (Windows/OS X/iOS/Android). We conveniently do this for you, though 🍻.
  • Scripting the VM with Frame Guest Agent Scripting.

Conclusion

Well, that's a wrap! We went from zero to “HOLY SMOKES! We can run No Man's Sky in the browser on a 5 year old Chromebook?! It can barely run on the Nintendo Switch!!!!!”. Joking aside, I think this API is powerful stuff! With about 100 lines of Javascript and 100 lines of CSS, we were able to accomplish a lot! Web developers that are more talented than I could/will make some knock-out experiences.

Anyways, If you've followed along through this exercise – thank you! 🍻 I hope you at least learned a bit about Frame or the web! If you've any questions, please feel free to reach out and ask me.

Lastly, below is a full copy of the code we created. Don't forget to fill in your own frameOptions, though!

Click here for the full source
<!-- 
*****
** Introduction to the Nutanix Frame Session API
** by Jason Thompson - jason.thompson@nutanix.com
*****
-->

<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello, Frame!</title>
<script src="https://unpkg.com/@fra.me/terminal-factory@2.48.3/api.js"></script>
<style>
body {
margin: 0;
font-size: 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
color: #feb17d;
height: 100vh;
overflow: clip;
background: rgb(55, 82, 109);
background: linear-gradient(
rgba(172, 129, 136, 1),
rgba(55, 82, 109, 1)
);
font-family: Arial, Helvetica, sans-serif;
}

p {
margin-top: 60px;
font-size: 2rem;
letter-spacing: -1px;
font-weight: 400;
text-shadow: -5px 5px #ac8188;
}

button#the-clicker {
background-color: #6768ab;
border: 2px rgba(255, 255, 255, 0.25) solid;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
color: white;
font-size: 1.5rem;
cursor: pointer;
transition: background-color 0.25s;
}

button#the-clicker:hover {
background-color: #7d7fd3;
border: 2px white solid;
}

button#the-clicker:disabled {
background-color: dimgrey;
border-color: darkgrey;
cursor: wait;
}

#events {
display: flex;
flex-wrap: wrap;
min-width: 100px;
margin-bottom: 60px;
width: 100%;
padding-left: 10px;
border-top: 1px rgba(214, 214, 214, 0.301) solid;
padding-top: 20px;
}

#events div:last-child {
font-weight: bold;
animation: pulse 3s ease infinite alternate;
font-style: italic;
}

#events div:last-child:after {
content: none !important;
}

#events div {
height: 40px;
line-height: 40px;
display: inline-block;
position: relative;
background-color: #0d2432;
color: white;
padding: 0 10px;
margin-left: 30px;
margin-bottom: 10px;
}

#events div:after {
color: #0d2432;
border-left: 20px solid;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
display: inline-block;
content: "";
position: absolute;
right: -20px;
top: 0;
}

@keyframes pulse {
0%,
100% {
background-color: #0d2432;
}

50% {
background-color: #37526d;
}
}
</style>
</head>

<body>
<p>This is a really simple HTML file.</p>
<button id="the-clicker" disabled>1-Click simplicity</button>
<div id="events">
<div>Welcome!</div>
</div>
<script type="text/javascript">
const clicker = document.getElementById("the-clicker");
const frameOptions = {
serviceUrl: "https://cpanel-backend-prod.frame.nutanix.com/api/graphql",
terminalConfigId: "YOUR-TERMINAL-CONFIG-ID",
token: "REPLACE-WITH-YOUR-TOKEN",
};

function addEventItem(event) {
const eventContainer = document.getElementById("events");
const eventDiv = document.createElement("div");
eventDiv.innerHTML = event;
eventDiv.classList.add("event-item");
eventContainer.appendChild(eventDiv);
}

document.addEventListener("DOMContentLoaded", async function () {
console.info("This page has fully loaded.");
const { TerminalEvent } = FrameTerminalApi;
try {
const terminal = await FrameTerminalApi.createInstance(frameOptions);
const clicker = document.getElementById("the-clicker");

terminal.bind(TerminalEvent.SESSION_STARTING, function (event) {
console.info(
"Session is starting! This may take up to 2 minutes..."
);
addEventItem(
"Session is starting! This may take up to 2 minutes..."
);
clicker.disabled = true;
});

terminal.bind(TerminalEvent.SESSION_STARTED, function (event) {
console.info("Session has successfully started!");
addEventItem("Session has successfully started!");
});

terminal.bind(TerminalEvent.SESSION_RESUMING, function (event) {
console.info("Resuming an existing session!");
addEventItem("Resuming an existing session!");
clicker.disabled = true;
});

terminal.bind(TerminalEvent.SESSION_RESUMED, function (event) {
console.info("Successfully resumed an existing session.");
addEventItem("Successfully resumed an existing session.");
});

terminal.bind(TerminalEvent.SESSION_CLOSING, function (event) {
console.info("Session is closing!");
addEventItem("Session is closing!");
});

terminal.bind(TerminalEvent.SESSION_CLOSED, function (event) {
console.info("Session closed!");
addEventItem("Session closed!");
clicker.disabled = false;
});

terminal.bind(TerminalEvent.SESSION_DISCONNECTED, function (event) {
console.info("Session disconnected!");
addEventItem("Session disconnected!");
clicker.disabled = false;
});

terminal.bind(TerminalEvent.USER_INACTIVE, function (event) {
console.info(
"Did the user step away? No keyboard or mouse movement received in a while..." +
JSON.stringify(event)
);
addEventItem(
"User is a ghost! Keyboard and mouse lay dormant a little too long..."
);
});

clicker.addEventListener("click", async function (event) {
event.preventDefault();
try {
const session = await terminal.getOpenSession();
if (session && session.state === "OPEN") {
await terminal.resume(session.id);
} else {
await terminal.start();
}
} catch (error) {
addEventItem(`Error: ${error.message}`);
console.error(`Something went wrong: ${JSON.stringify(error)}`);
}
});
clicker.disabled = false;
} catch (error) {
if (error.networkError && error.networkError.statusCode === 401) {
console.log("Error: 401 Unauthorized. Token expired.");
addEventItem(`Error: JWT is expired or invalid.`);
} else if (
error.networkError &&
error.networkError.statusCode === 403
) {
console.log("Error: 403. Forbidden/Permission denied.");
addEventItem(
`Error: Permissions denied. Make sure you have access to the Account/Launchpad.`
);
} else {
console.error(`Something went wrong: ${error}`);
addEventItem(`Error: ${error.message}`);
}
}
});
</script>
</body>
</html>

Author

Jason Thompson
Jason is a Senior Sales Engineer and Full-stack Software Engineer for Dizzion, working with enterprises and software companies along with Dizzon's robust APIs to deliver delightful end-user experiences. Wielding years of experience with VDI, the Web, a wide variety of APIs and automation, multimedia, and design, Jason has built and contributed to many internal tools and critical aspects of Dizzion. With help and support of his colleagues, Jason's notable contributions include the introduction of installable PWAs (for applications and desktops on any OS), enhancements to authentication and session start behaviors, OS integrations, admin UX, and sizable chunks of Dizzion's public documentation, examples, and code for our most valued customers.