Metrics in Scoped Apps

EdwinCoronado
ServiceNow Employee
ServiceNow Employee

Hello,

I have a table (doesn't extend Task) in a scoped app that has a "State" field which I want to keep track of time of how long a record spends in each of these states. In the ServiceNow documentation for Metric definitions I found the following:

Note: In the base system, metrics are configured to work on the task table only. To apply metrics to cmdb_ci tables, duplicate the metric events business rule that currently runs on the task table for the cmdb_ci table. Without the events created, no metric processing can occur.

However, when I look at the "metric events" business rule in the Task table, I see that it's inserting an event (metric.update) which is in the global scope. When I then look at the Script Action that runs when that event is inserted, I see that it instantiates an object (script include) called "MetricInstance". This script include is also only available in the Global scope, and the table that it writes to (metric_instance) is ALSO only available in the global scope. I know that a "quick" solution would be to make the table and script include public for other scopes to use, but this is not an option in my case. 

Any help would be appreciated! 

21 REPLIES 21

Well, I have a scoped application with a table which is not extended from task... I'm already getting tired just writing that sentence 🙂

I started by creating a metric definition which is very basic, but no instances was created. After a bit of investigation I of cause found that it only works on tables extended from task. 

I copied the business rule "metrics events" as suggested in various posts and articles, but I'm stuck getting to script to work. I'll paste in the script below, but this is just a copy of the out of the box script. I'm failing to the the GlideScriptRecordUtil class to work in a scoped application.

Somewhere else in the community a guy proposed to create a new script include in global scope which is callable from a scope, which then calls the GlideScriptRecordUtil, but after struggling with that for way too many hours, I gave up.

queueMetricUpdate();

function queueMetricUpdate() {    
	var gru =  new GlideScriptRecordUtil.get(current);
	var fieldsChanged = gru.getChangedFieldNames();
	var gr = getDefinitions(fieldsChanged);
	fields = '';
	while (gr.next())
		fields += gr.field + ',';
	
	if (fields.length > 0) {
		fields = '[' + fields.substring(0, fields.length - 1) + ']';
		gs.eventQueue('metric.update', current, fields, current.sys_mod_count, 'metric_update');
	}
}

function getDefinitions(fields) {
	var gr = new GlideAggregate('metric_definition');
	gr.addActiveQuery();
	var tables = GlideDBObjectManager.getTables(current.getTableName());
	gr.addQuery('table', tables);
	gr.addQuery('field', fields);
	gr.groupBy('field');
	gr.query();
	return gr;
}

If you have any ideas I'll be happy to have another go at this. Otherwhise I'm close to simply create some extra date fields on that table which gets updated on state change.. I would just hate that solution.

 

I have this working at work, but.. I wasn't able to get it to work.  I think they just gave access to the script include or made a shim for it.  I have it in my list of things to do.  Hopefully by end of the week I'll have it redone in my PDI so I can write about it.

Awesome!

But yeah, the catch is that GlideScriptRecordUtil is not a script include. 

This has been in my queue for a bit to figure out.

I tried this again with a fresh scoped application and this is not currently possible to do entirely within a scope.  At some point you need to either shim in access to a script include or create something in global.

I'll walk through the working solution for whatever poor soul happens across this.

  1. Create a scoped application.
  2. Create a table called "Content"
  3. Add two fields, "Title" (string), "Stage" (choice)
  4. Give Stage a few choices.
  5. We're done in the scope.  
  6. Switch to global
  7. Copy the rule "metric events", and point it to your scoped table.
  8. That's it.

Now I feel like I need to argue with myself.

"Come on, it can't be impossible."

Okay, lets walk through the last 3 hours of my life.

The business rule

I created a scope, table, copied the business rule.

Created a Fix Script to test running this quickly.  

 

`GlideScriptRecordUtil` is not available in scopes. 


So this was used to get the fields that changed.  Okay some work and I got a working scoped version here.

function getChangedFieldNames(gr) {
        var result = [];
        var elements = gr.getElements();
        gs.info('looping over fields looking for chagnes');
        var size = elements.length;
        for (var i = 0; i < size; i++) {
            var ge = elements[i];
            gs.info('ge.getName(): ' + ge.getName() + " changed: " + ge.changes());
            if (ge.changes()) {
                gs.info('saw field [' + ge.getName() + '] change');
                result.push(ge.getName());
            }
        }
        return result;
    }

Once that's added to the business rule we can call it instead of GlideScriptRecordUtil.

//var gru = new GlideScriptRecordUtil.get(current);
//var fieldsChanged = gru.getChangedFieldNames();
var fieldsChanged = getChangedFieldNames(current);

addActiveQuery is not available in scopes.

Okay updated the `getDefinitions` function to use gr.addQuery('active', 'true');

`GlideDBObjectManager.getTables()` is not available in scope.

There's a replacement 

`var tables = new GlideTableHierarchy(table);`
Then when you would have called tables before you call tables.getTables();
 

metric.update is not availabe or found for scope.

So either I don't know how to give permission to generate a global event OR I need to create a scoped version of the event and trigger it.
Created a the scoped version of the event and updated the gs.eventQueue line to 
`gs.eventQueue('x_8821_testmetric.metric.update', current, fields, current.sys_mod_count, 'metric_update');`
 
At this point you have a working rule that *should* work if you can get the event to process.  Since this is a different script I'm breaking this up here.
 

The Script Action

 
Created a copy of the script action that processes metric.update
 
Because it's async, created another fix script and emulated running the code there.
 
 

`GlideDBObjectManager.getTables` not available in scope

 
We solved this before, so just solved that the same way.  Well nearly, I passed in a table since I was having some other problem with my fix script.
    //var tables = GlideDBObjectManager.getTables(current.getTableName());
    var tables = new GlideTableHierarchy(table);
    gr.addQuery('table', tables.getTables());
 Nail in the coffin is next.
 

GlideRecordRollback not available in scope

 
I don't see a way around this unless someone reverse engineers how the `toVersion` function works.  I looked and didnt find anything.  
 
If anyone else wants to try this please have it and comment here so I can follow along with my ????.

I got it now working with:

1. This Business Rule:

queueMetricUpdate();

function queueMetricUpdate() {
    //var gru =  new GlideScriptRecordUtil.get(current);
    //var fieldsChanged = gru.getChangedFieldNames();
    var fieldsChanged = getChangedFieldNames(current);
    var gr = getDefinitions(fieldsChanged);
    fields = '';

    while (gr.next())
        fields += gr.field + ',';

    if (fields.length > 0) {
        fields = '[' + fields.substring(0, fields.length - 1) + ']';
        gs.eventQueue('sn_vdr_risk_asmt.vrm.metric.update', current, fields, current.sys_mod_count, 'metric_update');
    }
}

function getDefinitions(fields) {
    var gr = new GlideAggregate('metric_definition');
    gr.addQuery('active', 'true');
    //var tables = GlideDBObjectManager.getTables(current.getTableName());
    var tables = new GlideTableHierarchy(current.getTableName());
    gr.addQuery('table', tables);
    gr.addQuery('field', fields);
    gr.groupBy('field');
    gr.query();
    return gr;
}

function getChangedFieldNames(gr) {
    var result = [];
    var elements = gr.getElements();
    var size = elements.length;
    for (var i = 0; i < size; i++) {
        var ge = elements[i];

        if (ge.changes()) {
            result.push(ge.getName());
        }
    }
    return result;
}

2. Create the Metric Definiton like this:

JulianLemcke_0-1675508808241.png

3. Created the new Script Action

4. Create a new Event Registry

 

As you can see on number two I've also been able to get the metrics populated properly as well as the script running to close the definitions in case the status = closed.