GlideAjax Response Null - Script Include not running?

Hurleybird
Tera Expert

Hi there, I'm a newbie that feels like I'm trying to do something beyond my skills - so I apologise in advanced if I'm missing any real basics.

Ultimate Goal: Use a Client Script to trigger a Guided Tour when a Case is in a specific state. Once the Tour has been viewed (tourCompleted) or dismissed (tourDismissed), the tour should not start.

I've used a script from Community Member athm (https://community.servicenow.com/community?id=community_article&sys_id=4d846de3db47130023f4a345ca961...) to achieve the conditional triggering. This works like a charm!

I plan to write an entry to "sys_guided_tour_user_overrides" if the user views or dismisses the tour. For now, I have manually added the entry in the table to test whether I can read from the table:

find_real_file.png

In order to read from the table, I was originally planning to utilise the Script Include "GTAutoLaunchController". However, rather than messing with a "system" Script, I created my own version of it, to allow me to make it Client Callable and mess around with it. From the Client Script, I am attempting a GlideAjax call. But no matter what I have tried, I believe that it's not triggering the script.

I have added gs.info in various places within the Script Include in an attempt to help me troubleshoot, but nothing appears in the System Log (syslog.list).

The Client Script is as follows:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
   if (isLoading || newValue === '') {
      return;
   }
	
	//If the current state of the form meets your conditions, launch the tour
if(g_form.getValue('resolution_code') != '0'){
	
	var ga = new GlideAjax('sn_tourbuilder.GTAutoLaunchUtils');
	ga.addParam('sysparm_name', 'getOverriddenToursForUser');
	ga.addParam('sysparm_userID', g_user.userID);
	ga.getXML(checkOveride);
	g_form.addInfoMessage(JSON.stringify(ga));
}
	
	function checkOveride(response) {
		g_form.addInfoMessage("Response was: " + JSON.stringify(response));
		var answer = response.responseXML.documentElement.getAttribute("tourId");
		g_form.addInfoMessage(JSON.stringify(answer));
// 		answer = answer.evalJSON();
// 		g_form.addInfoMessage(answer);
		
// 		for (var i=0 ; i < answer.length ; i++) { //Loop into the array
		
// 				g_form.addInfoMessage(answer[i].tourID);
		}
// 	}

top.NOW.guidedToursService.startTour("81996c861b515810e8a7eca13d4bcb6c", 0);
//(sys_id of the guided tour you want to launch, step#)

//Check if the guided tour is dismissed by the user, if so then end the tour. 
//If not the system will dispaly a message if the user navigates to a different form
//before the completion of the guided tour.

	setInterval(function(){
		if(top.NOW.guidedToursService.isDismissed){
			top.NOW.guidedToursService.endTour(); 

			clearInterval();
		}
		
	},2000);
}

The results from the InfoMessages are as follows:

g_form.addInfoMessage(JSON.stringify(ga));

{"contextPath":"xmlhttp.do","params":{"sysparm_processor":"sn_tourbuilder.GTAutoLaunchUtils","sysparm_scope":"sn_customerservice","sysparm_want_session_messages":true,"sysparm_name":"getOverriddenToursForUser","sysparm_userID":"a1fa60be1b6e40d044addac3cc4bcbb9"},"encodedString":"","encode":true,"processor":"sn_tourbuilder.GTAutoLaunchUtils","wantRequestObject":false,"runRequestInBatch":false,"wantAnswer":false,"async":true}
 
I've bolded what I believe are the key parameters, that seems to suggest it's passing what I believe is required to trigger the script and pass the correct variables?
 
g_form.addInfoMessage("Response was: " + JSON.stringify(response));
 
Info MessageResponse was: {}
 
g_form.addInfoMessage(JSON.stringify(answer));
Info Messagenull
 
The Script Include is as follows:
var GTAutoLaunchUtils = Class.create();
var tourTypes = {
	SERVICE_PORTAL: 'service_portal',
	PLATFORM: 'platform',
	CUSTOM: 'custom_ui'
};
GTAutoLaunchUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    type: 'GTAutoLaunchUtils'
});
GTAutoLaunchUtils.prototype.tableNames = {
	tours: 'sys_embedded_tour_guide',
	userOverrides: 'sys_guided_tour_user_overrides'
};
insertOverride = function(tourId, userId) {
	var rec = new GlideRecord(this.tableNames.userOverrides);
	rec.initialize();
	rec.tour = tourId;
	rec.user = userId;
	rec.disable_autolaunch = true;
	rec.insert();
};

removeOverrides = function(page, userId, portal) {
	var self = this;
	var pages = this.getToursForPage(page, portal);
	var ids = pages
				.map(function(d) { return d.id; })
	;
	if (ids.length) {
		var rec = new GlideRecord(self.tableNames.userOverrides);
		if (userId) {
			rec.addQuery('user', '=', userId);
		}
		rec.addQuery('tour', 'IN', ids.join(','));
		rec.deleteMultiple();
		return {ids: ids};
	} else {
		return null;
	}
};

overrideTourForUser = function(tourId) {
	var currentUser = gs.getUser();
	var rec = new GlideRecord(this.tableNames.userOverrides);
	rec.addQuery('user', '=', currentUser.getID());
	rec.addQuery('tour', '=', tourId);
	rec.query();
	var exists = rec.next();
	var tourRecord = new GlideAggregate(this.tableNames.tours);
	tourRecord.addQuery('sys_id', '=', tourId);
	tourRecord.query();
	if(tourRecord.hasNext() && !exists) {
		this._insertOverride(tourId, currentUser.getID());
		return {msg: this.messages.done};
	} else if(tourRecord.hasNext() && exists && !rec.disable_autolaunch){
		rec.disable_autolaunch = true;
		rec.update();
		return {msg: this.messages.done};
	}else {
		return null;
	}
};

overrideAllToursForUserInPage = function(page, portal) {
	var self = this;
	var currentUser = gs.getUser();
	var userId = currentUser.getID();
	var res = this._removeOverrides(page, userId, portal);
	if (res) {
		res.ids.forEach(function(id) {
			self._insertOverride(id, userId);
		});
		return {msg: self.messages.done};
	} else {
		return null;
	}
};

getOverriddenToursForUser = function() {
	gs.info("The script ran");
 	var userID = this.getParameter('sysparm_userID');
	var gr = new GlideRecord("sys_guided_tour_user_overrides");
  var data = [];
  gr.addQuery('user', userID);
  gr.addQuery('disable_autolaunch',true);
  gr.query();
	gs.info("This is killing me, will this work " + gr.tour);
  while(gr.next()) {
	gs.info("Results from this script " + gr.tour);
	var elem = {tourId: '' + gr.tour};
	data.push(elem);
  }
	return data.toString();
};

I have tried to follow all of the advice in GlideAjax Troubleshooting Guide, I've attempted multiple variations on passing arrays, JSON etc. I've tried different methods of logging, but I believe that gs.info is correct for a Scoped Application.

Ultimately, I'm not convinced that the script is being called (due to the lack of logging). I have run a couple basic Background Scripts (in Global scope) to test whether I can indeed get a result back from the table - and I can:

var gr = new GlideRecord('sys_guided_tour_user_overrides');
gr.addQuery('user','a1fa60be1b6e40d044addac3cc4bcbb9');
gr.addQuery('disable_autolaunch',true);
gr.query();
while(gr.next()){
gs.info("Tour ID: " + gr.tour);
}

Returns:

*** Script: Tour ID: 81996c861b515810e8a7eca13d4bcb6c
var gr = new GlideRecord('sys_guided_tour_user_overrides');
gr.addQuery('user','a1fa60be1b6e40d044addac3cc4bcbb9');
gr.addQuery('disable_autolaunch',true);
gr.query();
var data = [];
while(gr.next()){
var elem = {tourId: '' + gr.tour};
data.push(elem);
}
data.toString();
gs.info(JSON.stringify(data));

Returns:

*** Script: [{"tourId":"81996c861b515810e8a7eca13d4bcb6c"}]

I'm totally at a loss and hope you can help/point me in the right direction.

Thanks!
1 ACCEPTED SOLUTION

DirkRedeker
Mega Sage

Hi

I have not tested out, but I think, you closed your Prototype definition TOO early (look at my screenshot).

find_real_file.png

You need to put all the code within the curly braces, to make them belong to the Class. You put all of your functions outside, which will make them "unreachable".

 

Below, I copied the starting part of an OOB AJAX Script include. There you can see, that all functions are INSIDE.

find_real_file.png

Additionally, make sure to name your Script Include EXACTLY as you do in the code.

 

 

Let me know if that answers your question and mark my answer as correct/helpful.

Have fun & Enjoy ServiceNow

BR

Dirk

View solution in original post

14 REPLIES 14

find_real_file.png

I've also tried:

var elem = {(gr.tour)};

With the same effect 😕

Hi 

It seems like you have a few steps that are not necessary. If you want to return only the tour name from multiple guided tour records you can push them into the array with data.push(gr.getDisplayValue(tour)). When returning that value to the client script you convert the array to a string with data.toString(). Once back in your client script you can get the answer back to an array which you can loop through using answer.join(','). It makes sense to create an array of objects if you want to return multiple values from multiple records. Otherwise making a simple array works best. 

--David

Hurleybird
Tera Expert

Looks like it this line was screwing it up - whether this was because it is a scoped application, I'm not sure?

var json = new JSON();

Now, I am sure I tried working around that with the scoped version JSON API Documentation:

var json = new global.JSON();

And that still failed. In the end, when I commented the line out, it passed through to the Client Script

🤦‍♂️

Hi

In my answer above, I was focussing on getting the AJAX call on the road.

Now, that I can see you having trouble with the implementation, I am wondering, if you can reach what you want in a much simpler way.

I never fired Guided Tours the way you do here, but I like that approach.

I guess, you want to just find out with your AJAX call is, if there is a record in the [sys_guided_tour_user_overrides] table for the current user and the current guided tour.

If that is the case, I recommend just to create a specific public function in your AJAX Script Include just for that.

I recommend to use the "User" and the "Tour" (Sys_ID of the tour) as parameters for your AJAX function, which in turn should just return true or false.

Returning "true" or "false" is easy to send back and easy to consume.

If you want to return it as String, you can go with "YES" and "NO" as return string and work on that in the client.

Does that make sense?

Let me know, we will make it!

BR
Dirk

Hurleybird
Tera Expert

Hi Dirk, firstly - yes - completely agree. The reason for posting was that I simply could not get the Script Include to fire, and you nailed the issue. Huge thanks for that, I really don't think I'd have found that, as I'd assumed because I'd "borrowed" it from ServiceNow code, it was going to be fine.

What you’re suggesting with the is in effect what I’ve done, yes. My code was at an early stage when I shared it....Because I’m brand new to all of this, I like to get the code working in phases - with the end goal always in mind. That way, I only have one part I need to troubleshoot. 😀

I do prefer your approach of returning True or False though. At the moment I have a bit of a cludge, due to passing the JSON string; no results end up returning 2 chars: “[]” so if answer.length < 3 suffices as a test.

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
	if (isLoading || newValue === '') {
		return;
	}
	
	var tourID = ("81996c861b515810e8a7eca13d4bcb6c");
	
	//If Resolution code changes from "-- None --" check if user has viewed the Tour
		if(g_form.getValue('resolution_code') != '0'){
	
			// Pass UserID, TourID to GTAutoLaunchUtils ScriptInclude
			var ga = new GlideAjax('sn_tourbuilder.GTAutoLaunchUtils');
			ga.addParam('sysparm_name', 'getOverriddenToursForUser');
			ga.addParam('sysparm_userID', g_user.userID);
			ga.addParam('sysparm_tourID', tourID);
			ga.getXML(checkOveride);
		}
	
	//If any value is returned, the user has not viewed the Tour
	function checkOveride(response) {
		var answer = response.responseXML.documentElement.getAttribute("answer");
		
		if (answer.length < 3){
			top.NOW.guidedToursService.startTour(tourID, 0);
			//(sys_id of the guided tour you want to launch, step#)

			//Check if the guided tour is dismissed by the user, if so then end the tour. 
			//If not - system will show a message if the user navigates to a different form
			//before the completion of the guided tour.

			var timer = setInterval(function(){
				if(top.NOW.guidedToursService.isDismissed){
					top.NOW.guidedToursService.endTour(); 
					jslog("Tour Dismissed");
					clearInterval(timer);
				}
				if(top.NOW.guidedToursService.isCompleted){
					jslog("Tour Completed");
					clearInterval(timer);
				}
			},2000);
		}
	}
}

The next challenge is fathoming out how to detect if the user completed the tour. I can’t find any documentation for the line: top.NOW.guidedToursService.isDismissed So I’ll either have to figure if the Event Listeners will help or continue digging for where the original poster found .isDismissed and see if there are other functions like .Completed or .wasCompleted or some such! I have slightly enhanced the original code as they hadn’t given the setInterval a name, so the ClearInterval wasn’t honoured.