Missions Web Component Click Through - Code Sample
The Missions web component has a built-in interface for action logging on linked mission rules (rules that have URLs for metadata). In Bunchball Go, a HUB_CLICK_THROUGH action is logged when a user clicks on a linked mission task. The action is logged prior to routing the user to the URL's destination.
Using the Missions web component in a non-Bunchball Go program requires you to implement both a client and server side solution. In Impact, a self-reporting rule should be used to log the action when a user marks the task as complete.
Note: Tracking a click through action does not guarantee that the user consumed the content. However, the Quiz web component can be used in parallel to test content proficiency.
The Missions web component displays an arrow in the detail view's task list to indicate the rule is linked to other content.
Use Case
Adding a linked rule to a mission allows the user to find the content required for the task easily and to be rewarded for accessing it. You may want to track a click through action if:
- your system cannot track or send notification of a view back to Nitro
- your system would require significant custom development to track or send notification of a view back to Nitro
Process Flow
The following steps need to occur to trigger a Nitro action.
-
The user clicks the link in the Missions web component, which triggers an event to the browser.
-
Client-side code detects the event and initiates the call to the server. See the Client Side Implementation section.
-
Server-side code sends the click through action to the Nitro API. See the Server Side Implementation section.
Client Side Implementation
The client side implementation requires the window.logAction function. The Missions web component looks for this function when users click a linked rule. If the function exists, it is invoked and sends the metadata as arguments. This function has one job, to relay the action data to Nitro so that it can log the action on behalf of the user. It does this by performing an XHR or ajax request to the server side endpoint defined in the server side implementation section.
JavaScript Code Example
function makeRequest(url, opts, callback){
var options = opts || {};
var request = new XMLHttpRequest();
request.open(opts.method || "GET", url, true);
request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
request.onload = function(){
if(request.status >= 200 && request.status < 400) {
// Success!
var data = JSON.parse(request.responseText);
if(callback){
callback(request, data);
}
}
else if(callback){
callback(request, null);
}
}
request.onerror = function(err) {
// There was a connection error of some sort
if(callback){
callback(request, null, err);
}
}
if(options.data || (options.method == "POST" || options.method == "PUT")) {
request.send(JSON.stringify(options.data));
} else {
request.send();
}
}
function logAction(actionTag, data, callback){
var url = '/actions/' + actionTag;
makeRequest(url, {
method: "POST",
data
}, callback)
}
Server Side Implementation
The server side implementation should be a restful endpoint that accepts and returns JSON. For example, if the defined route is /actions, then actions should receive a POST request. The payload should include the action name, the user ID, and url metadata. When the request is received, a new server side request should be made to get a 2-legged token. That token should be used to log the action for the user passing along the action tag and metadata.
The 2-legged credentials should not be exposed to the client. If they are, they can be retrieved and used to compromise the program.
Python Code Example
@plugin.route('actions/<action_tag>', methods=["POST"])
def log_action(action_tag):
action_logger = ActionLogger()
action_payload = request.get_json()
success = jsonify({"response": "Action logged", "code": 200}), 200
if current_user.is_authenticated:
elif action_tag == "HUB_CLICK_THROUGH":
action_logger.log_action(Actions.click_through, current_user, raise_exception=False, payload=action_payload) # noqa
return success
else:
current_app.logger.warning('Action not found {}'.format(action_tag))
return jsonify({"error": "Action not found", "code": 404}), 404
else:
current_app.logger.warning('User is not authenticated')
return jsonify({"error": "No authentication found", "code": 403}),
403
Server Side Action Logger Details
from enum import Enum
from urllib.parse import unquote
from src.config import Config
from src.exceptions import ApiGatewayException, SecurityError
from src.plugins.auth.oauth import OauthFactory
class Actions(Enum):
click_through = 'HUB_CLICK_THROUGH', 'Content clicked through'
def __new__(cls, name, description):
obj = object.__new__(cls)
obj._value_ = name
obj._description = description
return obj
def description(self):
return self._description
@staticmethod
def url(gamification_id):
return '{}/users/{}/actions'.format(Config.SERVICE_API_GATEWAY, gamification_id)
def payload(self, payload):
if payload:
return self.merge_dictionaries({'name': self._value_},payload)
return {'name': self._value_}
def merge_dictionaries(self, a, b, path=None):
if path is None: path = []
for key in b:
if key in a:
if isinstance(a[key], dict) and isinstance(b[key], dict):
self.merge_dictionaries(a[key], b[key], path + [str(key)])
elif a[key] == b[key]:
pass # same leaf value
else:
raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
else:
a[key] = b[key]
return a
class ActionLogger:
def __init__(self):
self.backend_oauth = OauthFactory().backend_oauth()
def log_action(self, action, user, raise_exception=False, payload={}):
if user:
# sanitize gamification_id from url encoded characters
user.gamification_id = unquote(user.gamification_id)
try:
url = action.url(user.gamification_id)
response = self.backend_oauth.post(user.ics, user.api_key, url, payload=action.payload(payload))
if response.status_code == 200:
return self.success(action, user)
self.failure(action, user, raise_exception=raise_exception)
except ApiGatewayException as e:
if raise_exception:
raise e
else:
current_app.logger.warning('failed to log action {}: {}'.format(url, e))
return
else:
if raise_exception:
raise SecurityError('User context unavailable. Please log in', 401)
@staticmethod
def success(action, user):
current_app.logger.info('logged action ({0}) for {1.ics}/{1.api_key}/{1.gamification_id}'.format(action.value, user))
@staticmethod
def failure(action, user, details={}, raise_exception=False):
current_app.logger.error('could not log action ({0}) for {1.ics}/{1.api_key}/{1.gamification_id}'
.format(action.value, user), **details)
if raise_exception:
raise ApiGatewayException(user.ics, user.api_key, user.gamification_id,
message="Unable to log {} action".format(action.value))
See also