Welcome back! This is second part of an introduction to the Amazon Connect Streams API. The first post is at: https://blogs.perficient.com/integrate/2017/10/05/intro-to-amazon-connect-streams-api-part-1/
https://github.com/phmiller/streams-examples
Source code and documentation for Streams is on GitHub: https://github.com/aws/amazon-connect-streams
2. Subscribing to Streams Events
You can see the full code for this sample by cloning my GitHub repository and checking out the “sample-2-subscribing-to-events” tag. Keep in mind that Streams applications, like Amazon Connect, only work in Chrome and Firefox
Streams provides events and subscription functionality to allow our application to react to changes to contacts (calls) and the logged in agent. This functionality is described in the Streams documentation at: https://github.com/aws/amazon-connect-streams/blob/master/Documentation.md#agent-api and https://github.com/aws/amazon-connect-streams/blob/master/Documentation.md#contact-api .
In this example, we will continue to display the standard CCP, but hook up methods in our application’s JavaScript to log contact and agent events and information. In a real application, we would take that information and do something with it. For example, take an attribute like the phone number of an account number we’ve looked up in a contact flow and do a screen pop.
Logging Events to Screen
We will start with some HTML work to give us a grid with 3 horizontally stacked panels: one for the CCP, one for logging application messages and another for logging Streams events as we handle them. I’m just showing the body tag here as we use CSS grid layout to give us our 3 horizontally stacked panels:
<body>
<div id="topDiv" style="width: 700px; min-width: 700px; margin-left: 200px;">
<div id="myApp">
<h1>Amazon Connect Streams API Samples</h1>
<h2>Subscribing to Streams Events</h2>
</div>
</div>
<div id="gridDiv" style="display: grid; grid-template-columns: 350px 400px 400px">
<div id="containerDiv" style="width: 320px; min-width: 200px; height: 465px; min-height: 400px; ">
<!--Amazon CCP will go here-->
</div>
<div id="logMsgsContainer">
<h2>Log Messages</h2>
<div id="logMsgs" style="height: 465px; overflow: auto;">
<!-- log messages will go here -->
</div>
</div>
<div id="eventMsgsContainer">
<h2>Event Messages</h2>
<div id="eventMsgs" style="height: 465px; overflow: auto;">
<!-- events from Streams API will go here-->
</div>
</div>
</div>
</body>
Subscriptions and Event Handlers
Next, back in the JavaScript code, after we’ve initialized Streams, we have to create subscriptions and handlers for Streams events. Make sure to check out the “Sidebar” below for some quirks with these events.
connect.contact(subscribeToContactEvents);
connect.agent(subscribeToAgentEvents);
function subscribeToContactEvents(contact) {
logInfoMsg("Subscribing to events for contact");
if (contact.getActiveInitialConnection()
&& contact.getActiveInitialConnection().getEndpoint()) {
logInfoMsg("New contact is from " + contact.getActiveInitialConnection().getEndpoint().phoneNumber);
} else {
logInfoMsg("This is an existing contact for this agent");
}
logInfoMsg("Contact is from queue " + contact.getQueue().name);
logInfoMsg("Contact attributes are " + JSON.stringify(contact.getAttributes()));
contact.onIncoming(handleContactIncoming);
contact.onAccepted(handleContactAccepted);
contact.onConnected(handleContactConnected);
contact.onEnded(handleContactEnded);
}
function handleContactIncoming(contact) {
if (contact) {
logInfoEvent("[contact.onIncoming] Contact is incoming. Contact state is " + contact.getStatus().type);
} else {
logInfoEvent("[contact.onIncoming] Contact is incoming. Null contact passed to event handler");
}
}
function handleContactAccepted(contact) {
if (contact) {
logInfoEvent("[contact.onAccepted] Contact accepted by agent. Contact state is " + contact.getStatus().type);
} else {
logInfoEvent("[contact.onAccepted] Contact accepted by agent. Null contact passed to event handler");
}
}
function handleContactConnected(contact) {
if (contact) {
logInfoEvent("[contact.onConnected] Contact connected to agent. Contact state is " + contact.getStatus().type);
} else {
logInfoEvent("[contact.onConnected] Contact connected to agent. Null contact passed to event handler");
}
}
function handleContactEnded(contact) {
if (contact) {
logInfoEvent("[contact.onEnded] Contact has ended. Contact state is " + contact.getStatus().type);
} else {
logInfoEvent("[contact.onEnded] Contact has ended. Null contact passed to event handler");
}
}
function subscribeToAgentEvents(agent) {
logInfoMsg("Subscribing to events for agent " + agent.getName());
logInfoMsg("Agent is currently in status of " + agent.getStatus().name);
agent.onRefresh(handleAgentRefresh);
agent.onRoutable(handleAgentRoutable);
agent.onNotRoutable(handleAgentNotRoutable);
agent.onOffline(handleAgentOffline);
}
function handleAgentRefresh(agent) {
logInfoEvent("[agent.onRefresh] Agent data refreshed. Agent status is " + agent.getStatus().name);
}
function handleAgentRoutable(agent) {
logInfoEvent("[agent.onRoutable] Agent is routable. Agent status is " + agent.getStatus().name);
}
function handleAgentNotRoutable(agent) {
logInfoEvent("[agent.onNotRoutable] Agent is online, but not routable. Agent status is " + agent.getStatus().name);
}
function handleAgentOffline(agent) {
logInfoEvent("[agent.onOffline] Agent is offline. Agent status is " + agent.getStatus().name);
}
function logMsgToScreen(msg) {
logMsgs.innerHTML = '<div>' + new Date().toLocaleTimeString() + ' ' + msg + '</div>' + logMsgs.innerHTML;
}
function logEventToScreen(msg) {
eventMsgs.innerHTML = '<div>' + new Date().toLocaleTimeString() + ' ' + msg + '</div>' + eventMsgs.innerHTML;
}
function logInfoMsg(msg) {
connect.getLog().info(msg);
logMsgToScreen(msg);
}
function logInfoEvent(eventMsg) {
connect.getLog().info(eventMsg);
logEventToScreen(eventMsg);
}
There’s a lot there, I know, but it all follows a simple pattern. Register a method to be notified of new contacts or agents. When you get a new contact or agent, subscribe to events from that object and then do something, in our case just log the event and some contact or agent data.
New Contacts or Agents
The methods connect.contact(function (contact)) and connect.agent(function (agent)) register the given method to be invoked by Streams when there’s a new contact or a new agent. The connect.contact handler will be invoked for a new incoming contact to the agent or if the agent has an existing contact (for example in after call). The connect.agent handler will be invoked once for the logged in agent.
Handling Events
Once our code has been notified by Streams of a new contact or agent, we subscribe to events from those objects. For example contact.onAccepted(function(contact)) to be notified when an we accept an incoming contact. And agent.onRefresh(function(contact)) to be notified when agent data changes. A full list of these events is in the GitHub documentation.
Logging
If you look at a handler like handleAgentRoutable you can see that we are just logging this event and some agent data to screen. The logInfoEvent and logInfoMsg methods display messages in the div’s we set up earlier.
function handleAgentRoutable(agent) {
logInfoEvent("[agent.onRoutable] Agent is routable. Agent status is " + agent.getStatus().name);
}
Sidebar: Quirks to Watch Our For
- Unfortunately the documentation is incorrect for the contact.getState and contact.getStateDuration methods. These methods are actually called contact.getStatus and contact.getStatusDuration respectively
- contact.OnIncoming is not invoked when a contact is incoming. Rather, you see a new incoming contact first when it hits the connect.contact handler
- contact.OnAccepted does not get the contact passed in as a parameter, whereas contact.OnEnded does. In general, I’d recommend you keep a copy of the contact object around that you first get in the connect.contact handler
- agent.getName only returns the Agent first name. This is what the documentation refers to as the “friendly display name”. There does not appear to be a way to edit this through the Amazon Connect website
Incoming Phone Number and Contact Attributes
While logging is great, we really want to do things like use the number of the incoming contact or attributes on that contact to do screen pops, run business logic or display extra information.
Avoid Contact Center Outages: Plan Your Upgrade to Amazon Connect
Learn the six most common pitfalls when upgrading your contact center, and how Amazon Connect can help you avoid them.
If you’ve looked into using Amazon Lambda within Amazon Connect contact flows, you might have seen that you get a JSON document full of contact info for each Lambda invocation. This includes the phone number and attributes from the contact flow. See https://docs.aws.amazon.com/connect/latest/adminguide/connect-lambda-functions.html for a full reference.
With Streams, you can get to the same information, but you access it through methods off of the contact object. For example the contact’s phone number is in the contact.getActiveInitialConnection().getEndpoint().phoneNumber property. Attributes are in a dictionary you get to from contact.getAttributes().
If you just want to see what’s available, you can invoke contact._getData() to get the internal data structure of the contact. This gives you an object very close to the JSON document from Lambda. From there you can find the property you need or format it with JSON.stringify to display on the screen. This is not recommended for a production application as the underscore indicates this method is meant to be private and its implementation could change in future releases.
In any case, you now know how to get to this information and I’ll leave it up to you what you do with it.
Go ahead and run our second sample application and you should see something like this:
Next, in the final sample application, we look at discarding the default CCP user experience and making our own custom one.
3. Creating a Custom User Experience
The first step in creating a custom contact control user experience is to hide the Amazon Connect CCP. We do this by adding the “display: none” style to the container div.
Next up is the HTML for our own controls. For this sample, I kept it simple, and yes, a bit ugly, adding a new “customCCPDiv” div to add the new user controls to. I added a place to display agent info and a couple of clickable span’s to serve as buttons for basic contact control:
<div id="containerDiv" style="width: 320px; min-width: 200px; height: 465px; min-height: 400px; display: none;">
<!--Amazon CCP is hiding in here-->
</div>
<div id="customCCPDiv" style="width: 320px; min-width: 200px; height: 465px; min-height: 400px; border-style: dotted; border-color: gray; border-width: thick;">
<!-- custom user experience goes here -->
<div id="statusDiv">
<div id="agentGreetingDiv" style="padding: 5px;">
<!-- say hi to the agent -->
</div>
<div id="agentStatusDiv" style="padding: 5px;">
</div>
<div id="goAvailableDiv" style="background-color: green; padding: 10px; margin: 10px 60px 10px 60px">
<span onclick="goAvailable()">Go Available</span><br />
</div>
<div id="goOfflineDiv" style="background-color: gray; padding: 10px; margin: 10px 60px 10px 60px">
<span onclick="goOffline()">Go Offline</span><br />
</div>
</div>
<div id="contactActionsDiv">
<div id="answerDiv" style="border-style: solid; border-color: black; border-width: medium; padding: 10px; margin: 10px 60px 10px 60px">
<span onclick="acceptContact()">Answer Incoming</span><br />
</div>
<div id="hangupDiv" style="border-style: solid; border-color: black; border-width: medium; padding: 10px; margin: 10px 60px 10px 60px">
<span onclick="disconnectContact()">Hang-Up</span><br />
</div>
</div>
</div>
Agent Status
For this sample, we are displaying the logged in agent’s name and current status. We are getting the initial state and agent’s name from the connect.agent handler and then setting the innerHTML property of the appropriate div’s:
function subscribeToAgentEvents(agent) {
window.myCPP.agent = agent;
agentGreetingDiv.innerHTML = '<h3>Hi ' + agent.getName() + '!</h3>';
logInfoMsg("Subscribing to events for agent " + agent.getName());
logInfoMsg("Agent is currently in status of " + agent.getStatus().name);
displayAgentStatus(agent.getStatus().name);
agent.onRefresh(handleAgentRefresh);
agent.onRoutable(handleAgentRoutable);
agent.onNotRoutable(handleAgentNotRoutable);
agent.onOffline(handleAgentOffline);
}
function displayAgentStatus(status) {
agentStatusDiv.innerHTML = 'Status: <span style="font-weight: bold">' + status + '</span>';
}
We are also calling displayAgentStatus in the onRefresh and other agent change handlers.
To set the agent’s status, we use Streams agent.setState method. This method is a bit tricky as you need to provide the target state object, not a string. You can get the target state object off of the logged in agent object. This is why we kept a reference to the logged in agent object from subscribeToAgentEvents in the window.myCPP object (see the full sample code to see where window.myCPP is initialized). With the agent object, we can get the agent state object we need, as shown below:
function goAvailable() {
var routableState = window.myCPP.agent.getAgentStates().filter(function (state) {
return state.type === connect.AgentStateType.ROUTABLE;
})[0];
window.myCPP.agent.setState(routableState, {
success: function () {
logInfoMsg("Set agent status to Available (routable) via Streams")
},
failure: function () {
logInfoMsg("Failed to set agent status to Available (routable) via Streams")
}
});
}
function goOffline() {
var offlineState = window.myCPP.agent.getAgentStates().filter(function (state) {
return state.type === connect.AgentStateType.OFFLINE;
})[0];
window.myCPP.agent.setState(offlineState, {
success: function () {
logInfoMsg("Set agent status to Offline via Streams")
},
failure: function () {
logInfoMsg("Failed to set agent status to Offline via Streams")
}
});
}
We first have to get an array of all the agent states from the agent object then filter it by the state.type to get the one we want.
We pass this state into agent.setState as well as an object of handler methods, one for a successful change, one if the setState fails.
Answering and Ending a Contact
To work with the current contact, we need to keep a reference to it handy, so we put in the window.myCPP object in subscribeToContactEvents:
function subscribeToContactEvents(contact) {
window.myCPP.contact = contact;
}
We can use the contact’s accept method to answer an incoming contact. The accept method takes an object of success and failure handlers.
Ending a contact is a bit trickier. There is a tempting contact.destroy method, but that will error out if we try to use it here. Instead, we go from the contact to the agent’s connection and destroy (hang-up) that individually. Once the agent hangs up, the customer is disconnected as well since there are no other agent’s involved.
function acceptContact() {
window.myCPP.contact.accept({
success: function () {
logInfoMsg("Accepted contact via Streams");
},
failure: function () {
logInfoMsg("Failed to accept contact via Streams");
}
});
}
function disconnectContact() {
//cannot do contact.destroy(), can only destroy (hang-up) agent connection
window.myCPP.contact.getAgentConnection().destroy({
success: function () {
logInfoMsg("Disconnected contact via Streams");
},
failure: function () {
logInfoMsg("Failed to disconnect contact via Streams");
}
});
}
Take the sample application for a spin and let me know if you see any issues. I hope this post has given you a good introduction to the Amazon Connect Streams API. Good luck writing your own browser-based integrations!
To learn more about what we can do with Amazon Connect, check out Helping You Get the Most Out of Amazon Connect
Wonderful post, Peter!
One question: How do I get info on the phone number the contact actually dialed? I see I can get the customer phone number via “contact.getActiveInitialConnection().getEndpoint().phoneNumber”, but if agents are answering to multiple phone numbers through the same queue, which method gives me the phone number dialed by the customer?
Thanks for reading!
Off the top of my head.. if you were in a contact flow, you’d get that number from $.SystemEndpoint.Address. You could assign that to a contact attribute in the contact flow and then access it client side with contact.getAttributes().
Another option; it might be possible to get it client side by looking through the connections off the contact via contact.getConnections(). Look for one with a connection type of ConnectionType.INBOUND. Not sure on this one, let me know what you find out.
Hey this was really helpful!
I was struggling with understanding how the CCP API works, and this really clarifies what is coming back during all the callbacks. Thanks for putting this together!!
tried this example but CCP login page keeps on reloading/refreshing. how can I resolve this?