Scoped Application Properties, Part II

“Any daily work task that takes 5 minutes will cost over 20 hours a year, or over half of a work week. Even if it takes 20 hours to automate that daily 5 minute task, the automation will break even in a year.”
Breaking into Information Security: Crafting a Custom Career Path to Get the Job You Really Want by Josh More, Anthony J Stieber, & Chris Liu

So far, we have created the UI Action to produce our Setup Properties button, configured the conditions under which the button would appear, and built the Business Rule to ensure that all properties created for an application are placed in a Systems Properties Category of the same name. Before we jump into the business of building out all of the things that we want to automate through the push of that button, there are just a couple of more little odds and ends that we will want to take care of first. One thing that will be helpful in maintaining System Properties for the application will be to have the properties listed out as Related Lists. That’s accomplished fairly easily by selecting Configure -> Related Lists from the hamburger menu on the main Application configuration page:

Configuring Related Lists on the main Application page

This will bring up the Available and Selected Related Lists for an Application, and you just need to find the System Property->Application entry in the Available bucket, highlight it, and then use the right arrow button to push it over into the Selected bucket. Oh, and don’t forget to click on that Save button once you’re done.

Activating the System Properties Related List

One other little thing that would be handy would be a link to the property admin page somewhere on the main Application configuration page. Even though our setup automation will be creating a link to that page somewhere on the main navigation menu, it would still be handy to have a link to that same page right here where we are configuring the app. The format of the URL for that link in both instances is the address of the page, plus a couple of parameters, one for the page title and the other for the name of the Category:

/system_properties_ui.do?sysparm_title=<appName> Properties&sysparm_category=<appName>

To build a link to that page on the main Application configuration page can be easily accomplished with another UI Action similar to the first UI Action that we built for our button, but this time we will select the Form link checkbox instead of the Form button checkbox. The code itself is just constructing the URL and then navigating to that location:

function openPage() {
	var appname = document.getElementById('sys_app.name').value;
	var url = '/system_properties_ui.do?sysparm_title=' + encodeURIComponent(appname) + '%20Properties&sysparm_category=' + encodeURIComponent(appname);
	window.location.href = url;
}

There may be more elegant ways to do that, but this works, so that’s good enough for now. This link should show after the Application Properties have been configured, so the display rules are basically opposite of those for our button: the button will show until you use it and the link will only show after you use the button. We can pretty much steal the condition from other UI Action, then, and just flip the last portion that checks for the presence of the Category:

gs.getCurrentApplicationId() == current.sys_id && (new GlideRecord('sys_properties_category').get('name',current.name)) && (new GlideRecord('sys_properties_category_m2m').get('category.name',current.name))

Those of you who are paying close attention will notice that we also add yet one more check to our condition, and that was just to make sure that there was at least one System Property defined for the app. There is no point in bringing up the property value maintenance page if there aren’t any properties. Assuming that you have set up application properties and you have defined at least one property, you should see the new link appear down at the bottom of the main Application configuration page:

New link to the property value maintenance page

That should take care of all of those other little odds and ends … now all that is left is to build out the code that will handle all of those initial setup tasks. Last time, I said that we would take care of that here, but as it turns out, that that was a lie. We’ll have to take care of that next time.

Scoped Application Properties

“I have been impressed with the urgency of doing. Knowing is not enough; we must apply. Being willing is not enough; we must do.”
Leonardo da Vinci

Many times when I build a scoped application in ServiceNow I will end up creating one or more System Properties that are specific to the application. To allow application administrators to maintain the values for those properties, I will usually create a menu item for that purpose, either under the application’s menu section if there is one, or under the the general Systems Properties menu item if there is not. To set all of that up requires a certain amount of work, and being a person who likes to avoid work whenever possible, I decided that it might be worth looking into possible ways to automate all of that work so that I could accomplish all of those tasks with minimal effort. It’s not that I’m afraid of work — I can lay right down next to a huge pile of work and sleep like a babe — it’s just that if there is a way to automate something to save myself some of that time and effort, I’m all in.

So what exactly is it that I seem to keep doing over and over again whenever I start out on a new app? I guess the main thing is the menu option that brings up the System Properties related to the application. If the application happens to have a menu section of its own, then I usually add this down at the bottom of that section and label it Application Properties. For applications that do not have a menu section of their own, I will use the name of the application (plus the word Properties) for the label, and stick it in the existing System Properties section of the menu. Regardless of where the menu item lives, it will point to the stock ServiceNow System Properties value entry page, which is not driven by Scope, but by Category. In order to bring up just the properties for this particular application, I also need to create a Category, usually using the name of the application as the category name, and then put all of application’s properties in that Category. Of course, the easiest way to put all of an application’s properties into a specific category is to create a Business Rule that does just that whenever a new property is created. Now, you don’t want just anyone to be tinkering with these important properties, so yet another thing that I always end up having to create is a Role that can be used to secure access to the properties.

None of these things are complicated, but it all takes time, and the time adds up. If you could do it all with just the push of a button, that would be pretty sweet. So what would we need to do that? Well, right off the bat, I guess the first thing that you would need would be that button. Let’s create a UI Action tied to the application form. It doesn’t have to do anything at this point; we just want to get the button out there to start things off.

UI Action to Set Up Application Property Support

Open up the New UI Action form by clicking on the New button at the top of the UI Action list, and enter Setup Properties in both the Name and Action Name fields. Check the Form button checkbox and the Active checkbox and that should be enough to save the record and create the UI Action. Pull up any Scoped Application at this point, and you should see the new button at the top of the screen.

Setup Properties button on Custom Application page

You really only want to see that button on apps that haven’t already been set up for application-level properties. Once you push that button and everything gets set up, the button should go away and never be seen or heard from again. Pushing the button will initiate a number of different things, so there will be a number of potential things that you could check to see if it’s work has already been accomplished. One of the easiest is probably the presence of a System Properties Category with the same name as the application. To configure the UI Action to only appear if such a category does not yet exist, you can add this line to the Condition property of the UI Action:

gs.getCurrentApplicationId() == current.sys_id && !(new GlideRecord('sys_properties_category').get('name',current.name))

The above line also ensures that you are in the right scope to be editing the app, as you wouldn’t want anyone setting everything up under the wrong scope. So now we have the button and we have it set up so that it only shows up when it is needed. We also have to figure what, exactly, is going to happen when we click on the button, but that’s a little more complicated, so let’s just set that aside for now. A considerably easier task would be to create that Business Rule that makes sure that all System Properties created for this app get placed into the Category of the same name. That one is pretty straightforward.

(function executeRule(current, previous) {
	var sc = new GlideRecord('sys_scope');
	sc.get(current.sys_scope);  
	if (sc.name != "Global" && sc.name != "global") {
		var category = new GlideRecord('sys_properties_category');
		if (category.get('name', sc.name)) {
			var prop = new GlideRecord('sys_properties');
			prop.addQuery('sys_scope', current.sys_scope);
			prop.orderBy('suffix');
			prop.query();
			while (prop.next()) {
			    var propertyCategory = new GlideRecord('sys_properties_category_m2m');	
				propertyCategory.addQuery('property', prop.sys_id);
				propertyCategory.addQuery('category', category.sys_id);
				propertyCategory.query();
				if (!propertyCategory.next()) {
					propertyCategory.initialize();
					propertyCategory.category = category.sys_id;
					propertyCategory.application = current.sys_scope;
					propertyCategory.order = 100;
					propertyCategory.property = prop.sys_id;
					propertyCategory.insert();	
				}
			}
		}
	}
})(current, previous);

This is a fairly simple script that implements fairly simple rules: get the scope of the current property, make sure it isn’t global, see if there is a Category on file with the same name as the Scope, and if so, put the current property into that category. That’s it. Once this is active, whenever you create a non-global System Property, it will automatically be assigned to the Category created for that application.

This is probably a good place to stop for now. Next time out, we will get into the details of just what happens when you push that new button we created.

Testing ServiceNow Event Utilities

“Testing leads to failure, and failure leads to understanding.”
Burt Rutan

Now that we have put together a basic ServiceNow Event utility and added a few enhancements, it’s time to try it out and see what happens. There are actually two reasons that we would want to do this: 1) to verify that the code performs as intended, and 2) to see what happens to these reported Events once they are generated. We will want to test both the server side process and the client side process, so we will want a simple tool that will allow us to invoke both. One way to do that would be with a basic UI Page that contains a few input fields for Event data and a couple of buttons, one to report the Event via the server side function and another to report the Event using the client side function.

For the sake of simplicity, let’s just collect the description value from the user input and hard code all of the rest of the values. We could provide more options for input fields, but we’re just testing here, so this will be good enough to prove that everything works. We can always add more later. But for now, maybe just something like this:

Simple Event utility tester

The first thing that we will need is some HTML to lay out the page:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<script src="client_event_util.jsdbx"></script>
<div>
 <g:ui_form>
  <h4>Event Tester</h4>
  <label for="description">Enter some text for the Event details:</label>
  <textarea id="description" name="description" class="form-control"></textarea>
  <div style="text-align: center; padding: 10px;">
    <input class="btn" name="submit" type="submit" value="Client Side Test" onclick="clientSideTest();"/>
	 
    <input class="btn" name="submit" type="submit" value="Server Side Test"/>
  </div>
 </g:ui_form>
</div>
</j:jelly>

There’s really nothing too special here; just a single textarea and a couple of submit buttons, one for the client side and one for the server side. On the client side button we add an onclick attribute so that we can run the client side script. On the server side button, we just let the form submit to the server, and then run the server side script when we get to the other side. The client side script is similarly very simple stuff:

function clientSideTest() {
	ClientEventUtil.logEvent('event_tester', 'None', 'Client Event Test', 3, document.getElementById('description').value);
	alert('Event generated via Client Side function');
}

… as is the server side script:

if (submit == "Server Side Test") {
	new ServerEventUtil().logEvent('event_tester', 'None', 'Server Event Test', 3, description);
	gs.addInfoMessage('Event generated via Server Side function');
}

Now all we have to do is hit that Try It button on the UI page, enter some description text, and then click one of the submit buttons to see what happens. On the client side:

Client side Event test

… and on the server side:

Server side Event test

Now that we have generated the Events, we can verify that they were created by going into the Event Management section of the menu and selecting the All Events option. By inspecting the individual Events, you can also see that each Event triggered an Alert, and by setting up Alert Management Rules, these Alerts could drive subsequent actions such as creating an Incident or initiating some automated recovery activity. But now we are getting into the whole Event Management subsystem, which is way outside of the scope of this discussion. My only intent here was to demonstrate that your ServiceNow components can easily leverage the Event Management infrastructure built into the ServiceNow platform, and in fact, do it quite easily once you created a few simple utility modules to handle all of the heavy lifting. Hopefully, that objective has been achieved.

Just in case anyone might be interested in playing around with this code, I bundled the two scripts and the test page together into an Update Set.

Enhanced Event Management for ServiceNow

“Great things are done by a series of small things brought together.”
Vincent Van Gogh

The one property of a ServiceNow Event that we virtually skipped over last time was the additional_info property. This is pretty much a catch-all for any other thing that you might want to record along with the Event itself. The additional_info property is stored in the database as a JSON-formatted string, which you can instantiate in use and then access like any other Javascript object. By leveraging the additional_info property, we can inject standard elements into the Event so that the reporting module does not have to include the code to provide that information. One such bit of info could be details on the currently logged on user. Another might be a Stack Trace containing the details of how we arrived at the point of an Event occurring.

The one thing that we would not want to do, however, would be to overlay any information that the reporting entity has provided, so it will be important to first check for the presence of any data in the additional_info object before we set any values of our own. The first thing that we would have to do would be to check to see if an additional_info value was even provided, and that it was an object to which we could add additional values. Here is one way to approach such a check:

if (additional_info) {
	if (typeof additional_info != 'object') {
		additional_info = {info: additional_info};
	}
} else {
	additional_info = {};
}

This ensures that we have an object, and that we have preserved whatever non-object (string, boolean, number,etc.) values that may have been provided instead of an object. Once we know we have an object to work with, then we can check the object for other properties, and if not already provided, provide a standard value. For example, here is how we could potentially include the various details on the user:

if (!additional_info.user) {
	additional_info.user = {};
	additional_info.user.sys_id = gs.getUserID();
	additional_info.user.id = gs.getUserName();
	additional_info.user.name = gs.getUserDisplayName();
}

Injecting a Stack Trace could be handled in a similar fashion:

if (!additional_info.stackTrace) {
	additional_info.stackTrace = this.getStackTrace();
}

Of course, a server side Stack Trace is of little value if your issue is a client side Event, so you would probably want to snag a client side Stack Trace while you were on the client side, before you sent everything over to the server side to be reported. We can steal some of the code from our server side counterpart to enhance the client side function and turn it into something like this:

logEvent: function(source, resource, metric_name, severity, description, additional_info) {
	if (additional_info) {
		if (typeof additional_info != 'object') {
			additional_info = {info: additional_info};
		}
	} else {
		additional_info = {};
	}
	if (!additional_info.stackTrace) {
		additional_info.stackTrace = this.getStackTrace();
	}
	var ga = new GlideAjax('ServerEventUtil');
	ga.addParam('sysparm_name', 'logClientEvent');
	ga.addParam('sysparm_source', source);
	ga.addParam('sysparm_resource', resource);
	ga.addParam('sysparm_metric_name', metric_name);
	ga.addParam('sysparm_severity', severity);
	ga.addParam('sysparm_description', description);
	ga.addParam('sysparm_additional_info', JSON.stringify(additional_info));
	ga.getXML();
}

By creating a common Event reporting utility function and leveraging the additional_info property for specific selected values, virtually all of the Events reported by ServiceNow components can share a common set of properties. This creates opportunities for common Event processing scripts and generic reporting possibilities that would not exist if everyone were simply following their own unique approach to reporting Events. And once you establish an organizational standard for common values stored in the additional_info property, adding additional items of interest at a future point in time is simply a matter of updating the common routine that everyone calls to report Events.

We still need to put together that testing page that we talked about last time out, but at this point, I think that will have to be a project for another day

Update: There is an even better version here.

Event Management for ServiceNow

“If you add a little to a little, and then do it again, soon that little shall be much.”
Hesiod

The Event Management service built into ServiceNow is primarily designed for collecting and processing events that occur outside of ServiceNow. However, there is no reason that you cannot leverage that very same capability to handle events that occur in your own ServiceNow applications and customizations. To do that easily and consistently, it’s helpful to bundle up all of the code to make that happen into a function that can be called from a variety of potential users. A server-side Script Include can handle that quite nicely:

var EventUtil = Class.create();
EventUtil.prototype = {
	initialize: function() {
	},
	logEvent: function(source, resource, metric_name, severity, description) {
		var event = new GlideRecord('em_event');
		event.initialize();
		event.source = source;
		event.event_class = gs.getProperty('instance_name');
		event.resource = resource;
		event.node = 'ServiceNow';
		event.metric_name = metric_name;
		event.type = 'ServiceNow';
		event.severity = severity;
		event.description = description;
		event.insert();
	},
	type: 'EventUtil'
};

There are a number of properties associated with Events in ServiceNow. Here is the brief explanation of each as explained in the ServiceNow Event Management documentation:

VariableDescription
SourceThe name of the event source type. For example, SCOM or SolarWinds.
Source Instance (event_class)Specific instance of the source. For example, SCOM 2012 on 10.20.30.40
nodeThe node field should contain an identifier for the Host (Server/Switch/Router/etc.) that the event was triggered for. The value of the node field can be one of the following identifiers of the Host:
  • Name
  • FQDN
  • IP
  • Mac Address
If it exists in the CMDB, this value is also used to bind the event to the corresponding ServiceNow CI.
resourceIf the event refers to a device, such as, Disk, CPU, or Network Adapter, or to an application or service running on a Host, the name of the device or application must be populated in this field. For example, Disk C:\ or Nic 001 or Trade web application.
metric_nameUsed Memory or Total CPU utilization.
typeThe type of event. This type might be similar to the metric_name field, but is used for general grouping of event types.
message_keyThis value is used for de-duplication of events. For example, there might be two events for the same CI, where one event has CPU of 50% and the next event has CPU of 99%. Where both events must be mapped to the same ServiceNow alert, they should have the same message key. The field can be left empty, in which case the field value defaults to source+node+type+resource+metric_name. The message_key should be populated only when there is a better identifier than the default.
severitySeverity of the event. ServiceNow values for severity range from 1 – Critical to 5 – Info, with the severity of 0 – Clear. Original severity values should be sent as part of the additional information.
additional_infoThis field is in JSON key/value format, and is meant to contain any information that might be of use to the user. It does not map to a pre-defined ServiceNow event field. Examples include IDs of objects in the event source, event priority (if it is not the same as severity), assignment group information, and so on. Values in the Additional information field of an Event that are not in JSON key/value format are normalized to JSON format when the event is processed.
time_of_eventTime when the event occurred on the event origin. The format is: yyyy-MM-dd HH:mm:ss GMT
resolution_stateOptional – To indicate that an event has been resolved or no longer occurring, some event monitors use ‘clear’ severity, while other event monitors use a ‘close’ value for severity. This field is used for those monitors proffering the latter. Valid values are New and Closing.

Generating an Event in ServiceNow is simply writing a record to the em_event table. To reduce the amount of info that needs to be passed to the utility, our example function assumes a standard value for a number of properties of the Event, such as the event_class, node, and type, and leaves out completely those things that will receive a default value from the system such as message_key, time_of_event, and resolution_state. For our purpose, which is a means to generate internal Events within ServiceNow, we can accept all of those values as standard defaults. The rest will need to be passed in from the process reporting the Event.

For the source value, I like to use the name of the object (Widget, UI Script, Script Include, etc.) reporting the Event. For the resource value, I like to use something that describes the data involved, such as the Incident number or User ID. The source is the tool, and the resource is the specific data that is being processed by that tool. The other three data points that we pass are metric_name, severity, and description, all of which further classify and describe the event.

The example above takes care of the server side, but what about the client side? To support client-side event reporting, we can add an Ajax version of the function to our server-side Script Include, and then create a client-side UI Script that will make the Ajax call. The modified Script Include looks like this:

var ServerEventUtil = Class.create();
ServerEventUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {

	logClientEvent: function() {
		this.logEvent(
			this.getParameter('sysparm_source'),
			this.getParameter('sysparm_resource'),
			this.getParameter('sysparm_metric_name'),
			this.getParameter('sysparm_severity'),
			this.getParameter('sysparm_description'));
	},

	logEvent: function(source, resource, metric_name, severity, description) {
		var event = new GlideRecord('em_event');
		event.initialize();
		event.source = source;
		event.event_class = gs.getProperty('instance_name');
		event.resource = resource;
		event.node = 'ServiceNow';
		event.metric_name = metric_name;
		event.type = 'ServiceNow';
		event.severity = severity;
		event.description = description;
		event.insert();
	},

	type: 'ServerEventUtil'
});

To access this code from the client side of things, a new UI Script will do the trick:

var ClientEventUtil = {

	logEvent: function(source, resource, metric_name, severity, description, additional_info) {
		var ga = new GlideAjax('ServerEventUtil');
		ga.addParam('sysparm_name', 'logClientEvent');
		ga.addParam('sysparm_source', source);
		ga.addParam('sysparm_resource', resource);
		ga.addParam('sysparm_metric_name', metric_name);
		ga.addParam('sysparm_severity', severity);
		ga.addParam('sysparm_description', description);
		ga.getXML();
	},

	type: 'ClientEventUtil'
};

Now that we have created our utility functions to do all of the heavy lifting, reporting an Event is a simple matter of calling the logEvent function from the appropriate module. On the server side, that would something like this:

var seu = new ServerEventUtil();
seu.logEvent(this.type, gs.getUserID(), 'Unauthorized Access Attemtp', 3, 'User ' + gs.getUserName() + ' attempted to access ' + functionName + ' without the required role.');

On the client side, where we don’t have to instantiate a new object, the code is event simpler:

ClientEventUtil.logEvent('some_page.do', NOW.user.userID, 'Unauthorized Access Attemtp', 3, 'User ' + NOW.user.name + ' attempted to access ' + functionName + ' without the required role.');

To test all of this out, we should be able to build a simple UI Page with a couple of test buttons on it (one for the server-side test and one for the client-side test). This will allow us to both test the utility modules and also see what happens to the Events once they get generated. That sounds like a good project for next time out.

But wait … there’s more!

“Waiting is one of the great arts.”
Margery Allingham

Every once in a while, I will run into a situation where my code needs to wait for some other asynchronous process to complete. You don’t really want to go anywhere, and you don’t want to do anything. You just want to wait. On the client side, this is easily accomplished with a simple setTimeout. This is usually done with a basic recursive loop that repeats until the conditions are right to proceed.

function waitForSomething() {
	if (something) {
		itIsNowOkToProceed();
	} else {
		setTimeout(waitForSomething, 500);
	}
}

Unfortunately, on the server side, where this need usually arises, the setTimeout function is not available. However, there is a widely known, but poorly documented, GlideSystem function called sleep that you can use in the global scope to provide virtually the same purpose.

function waitForSomething() {
	if (something) {
		itIsNowOkToProceed();
	} else {
		gs.sleep(500);
		waitForSomething();
	}
}

Don’t try this in a scoped application, though, because you will just get a nasty message about how gs.sleep() is reserved for the global scope only. Fortunately, like a lot of things in ServiceNow, if you know the secret, you can get around such restrictions. In this case, all you really need is a Script Include that IS in the global scope, and then you can call that script from your scoped application and it will work just fine. Here is a simple example of just such as script:

var Sleeper = Class.create();
Sleeper.prototype = {
    initialize: function() {
    },

	sleep: function(ms) {
		gs.sleep(ms);
	},

    type: 'Sleeper'
};

Once you have your global-scoped Script Include set up, you can now call it from a scoped app and it will actually work.

function waitForSomething() {
	if (something) {
		itIsNowOkToProceed();
	} else {
		new global.Sleeper().sleep(500);
		waitForSomething();
	}
}

Hiding an Empty Shopping Cart

“I love it when a plan comes together.”
Col. John “Hannibal” Smith

On the stock header of a ServiceNow portal there is an image of a shopping cart. As you order things from the Service Catalog, the number of items in your cart is displayed in the lower right-hand corner of that image. If you didn’t come to the portal to order something, the empty cart just sits there at the top of the screen for no reason, so I thought it might be nice to just hide the cart until there was at least one item in it.

There are already a couple of other items in the header that only appear if there is something there, the list of Requests and the the list of Approvals. I figured that I could emulate the approach taken on those items and it would work the same way.

Most of the things that drive ServiceNow are built using ServiceNow, and you can find them in the system database and edit them to do what you want. Some things, however, are built into the system, and there is no way that you can modify them. There is also a third category, and that is those things that actually are housed in the system database, but are locked down as read-only or ACL-controlled such that you cannot modify them. The Header Menu widget that contains the shopping cart code happens to fall into that third category. There it was right in front of me, but it was not editable, even though I am a System Administrator and I was in the right Update Set.

Nothing motivates me more than being told that I am being prevented from doing something that I want to do, so I immediately went to work trying to figure out a way to make changes to the Header Menu widget. As it turns out, it wasn’t that hard to do.

The first thing to do was to use the hamburger menu to export the widget to XML:

Export to XML (This Record)

Once I had my own copy on my own machine, I could look at the code and figure out what need to be changed and then change it. Digging through the HTML, I found out that there was already an ng-show attribute on the shopping cart for another purpose, so I just needed to add my condition to the list, and that should do the trick. Here is the relevant section of code:

  <!-- Shopping cart stuff -->
 <li ng-if="::options.enable_cart && data.isLoggedIn" ng-show="::!accessibilityEnabled" class="dropdown hidden-xs header-menu-item" role="presentation">
  	<a href
       data-toggle="dropdown"
       id="cart-dropdown"
       uib-tooltip-template="'item-added-tooltip.html'"
       tooltip-placement="bottom"
       tooltip-trigger="'none'"
       tooltip-is-open="$parent.itemAddedTooltipOpen"
       title="${Your shopping cart currently has} {{cartItemCount}} ${items}"
       aria-label="${Shopping cart}"
       role="menuitem">
    	<i class="fa fa-shopping-cart" aria-hidden="true"></i>
      <span ng-bind-html="'${Cart}'" aria-hidden="true"></span>
      <span ng-if="cartItemCount > 0" aria-hidden="true" class="label label-as-badge label-primary sp-navbar-badge-count">{{cartItemCount}}</span>
		</a>
    <div class="dropdown-menu cart-dropdown">
      <sp-widget widget="data.cartWidget"></sp-widget>
    </div>
  </li>

A little further down in the code there was reference to a variable called cartItemCount. Using my puissant powers of deductive reasoning, I concluded, based on the name of the variable and its usage, that this variable contained the number of items in the cart. This is exactly what I needed for my condition, so I changed this line:

<li ng-if="::options.enable_cart && data.isLoggedIn" ng-show="::!accessibilityEnabled" class="dropdown hidden-xs header-menu-item" role="presentation">

… to this:

<li ng-if="::options.enable_cart && data.isLoggedIn" ng-show="cartItemCount > 0 && !accessibilityEnabled" class="dropdown hidden-xs header-menu-item" role="presentation">

Now that I have figured out what change to make and have implemented the change, all I need to do is get it back into the instance. Fortunately, ServiceNow provides a way to do that, and it has the added benefit of bypassing those pesky rules that were preventing me from editing the thing directly in the tool.

If you open the System Update Sets section and go all the way down to the bottom and select Update Sets to Commit, there will be a link down at the bottom of that page titled Import Update Set from XML. Click on that guy, and even though your exported and modified XML document is not technically an Update Set, you can pull it in via that process and it will update the widget with your changes.

Returning Exported XML Back to the Instance

Now when I am on a portal that uses the Header Menu widget, you only see the Shopping Cart when there is something in the cart. Whenever it is empty, it drops out of the view.

I like it!