Name

global.snd_Xplore

Description

Exploratory programming script used by Xplore UI. Can also be used anywhere new snd_Xplore().xplore(obj) will use gs.print to show the object and what it contains.

Script

var snd_Xplore = function () {
this.prettyPrinter = new snd_Xplore.PrettyPrinter();
this.default_reporter = 'snd_Xplore.PrintReporter';

/**
 * {RegExp}
 * Ignore list to prevent looking over inaccessible Java stuff.
 * The majority of these are fully capitalised properties found on Java Packages
 * which would otherwise throw an exception.
 * There is no point even adding these to the results list.
 * E.g. DB, Y, _XML, M2MMAINTAINORDER
 */
this.ignorelist_regexp = /^[A-Z0-9_$]+$/;
this.debug = false;
};
snd_Xplore._lists = null;


snd_Xplore.prototype.toString = function () {
return '[object ' + this.type + ']';
};
snd_Xplore.prototype.type = 'snd_Xplore';

snd_Xplore.prototype.setReporter = function (name) {
var o = snd_Xplore.dotwalk(typeof window === 'undefined' ? global : window, name);
if (typeof o !== 'function') throw new Error(name + ' is not a valid reporter class.');
this.reporter = new o();
return this.reporter;
};

/**
* Check if Xplore is being run in a scoped application.
*
* @return {Boolean}
*/
snd_Xplore.prototype.isInScope = function () {
if (typeof window !== 'undefined') return false;
return !('print' in gs);
};

/**
* Gets the name of the current scope. e.g. 'global' or 'x_abc_test'
*
* @return {string}
*/
snd_Xplore.prototype.getScopeName = function (scope) {
return scope === global ? 'global' : Object.prototype.toString.call().slice(8, -1);
};

snd_Xplore.prototype.debugMsg = function (msg) {
if (this.debug) {
  if (typeof window === 'undefined') {
    snd_Xplore.gsdebug(msg);
  } else {
    jslog(msg);
  }
}
};

/**
* summary:
*
*
* description:
*   The snd_xplore function here allows exploratory programming to take place
*   in ServiceNow. Simply call new snd_Xplore().xplore(my_obj) in a background
*   script and watch all the objects get printed out on screen.
* @summary: Iterate over any object to retrieve its contents.
*
* param: obj [Any]
*   The object you want to explore!
* param: reporter [String] Optional
*   The name of the custom reporter object so you can customise where the output gets sent.
* param: options [Object] Optional
*   Customise what happens using this options object.
*   -show_props: [Boolean]
*     Set false to disable attempting to parse through the objects' properties.
*     Defaults to true.
*   -no_quotes: [Boolean]
*     Set true to prevent quotes from being added to strings.
*     Defaults to false.
*   see: lookAt
*/
snd_Xplore.prototype.xplore = function (obj, reporter, options) {
var use_json,
    target;

options = options || {};
use_json = options.use_json;
if (typeof options.use_json === 'undefined') options.use_json = false;
this.debug = !!options.debug_mode;

this.prettyPrinter.noQuotes(options.no_quotes);

if (options.dotwalk) obj = snd_Xplore.dotwalk(obj, options.dotwalk);

// register the reporter on this object so it works in a scope
if (reporter || !this.reporter) {
  this.reporter = this.setReporter(reporter || this.default_reporter);
}
reporter = this.reporter;

this.debugMsg('Running xplore in debug mode...');

target = this.lookAt(obj, '[Target]', options);
reporter.begin(target);

this.debugMsg('Exploring object ' + target.name);

if (options.show_props !== false && obj !== null && obj !== undefined) {
  options.use_json = use_json;
  this.xploreProps(obj, reporter, options);
}

reporter.complete();
this.debugMsg('Xplore complete.');
};

snd_Xplore.prototype.xploreProps = function (obj, reporter, options) {
var lists = snd_Xplore.getLists();
var type = snd_Xplore.getType(obj);
var properties = snd_Xplore.getPropertyNames(obj);
var prop_type;
var result;
var name;
var i;

this.debugMsg('Exploring properties...');

reporter = reporter || {};
reporter.result = reporter.result || function () {};

for (i = 0; i < properties.length; i++) {
  name = properties[i];
  prop_type = undefined; // reset

  if (type.string == '[object String]' && name.match(/^[0-9.]+$/)) {
    // this is a letter in a string
    continue;
  }

  this.debugMsg('Looking at ' + name);

  if (lists.ignorelist.indexOf(',' + name + ',') > -1) {
    this.debugMsg(' - ignore listed');
    continue;
  }

  try {
    prop_type = snd_Xplore.getType(obj[name]);
    result = this.lookAt(obj[name], name, options);
    this.debugMsg(' - ' + result.type);
  } catch (ex) {
    this.debugMsg(' - Exception: ' + ex);
    result = {
      name: name,
      type: '[Restricted]'
    };
    result.string = this.formatException(ex);
  }

  reporter.result(result);
}

this.debugMsg('Done exploring properties.');
};

snd_Xplore.prototype.formatException = function formatException(ex) {
try {
  if (ex.message && ex.message.indexOf('Illegal access') > -1) {
    return '[Restricted]';
  }
} catch (e) {
  // prevent getMessage errors
}
return '[Property access error: ' + ex + ']';
};

/**
summary:
  The magic method that works out what any object is and even attempts to
  find it's contents.

description:
  Takes any object and an optional name of that object and returns
  a simple result object containing the details of what was found.

param: obj [Object]
  Any object that needs to be looked at.
param: name [String] Optional
  The name of the object to populate the result with.
param: options [Object] Optional
  An options object.

returns: Object
  An object containing the following properties:
    -name [String]
      The name of the object if it was provided, otherwise an empty string.
    -type [String]
      The class name of the object.
**/
snd_Xplore.prototype.lookAt = function (obj, name, options) {
var result = {},
    lists = snd_Xplore.getLists(),
    type;

this.prettyPrinter.noQuotes(options.no_quotes);

result.name = name || undefined;
result.type = '';
result.string = null;

// this covers an independent call to this function - handled by xploreProps
if (lists.ignorelist.indexOf(',' + name + ',') > -1) {
  result.type = '*IGNORE LISTED*';
  return result;
}

// The try/catch is required for things like new GlideDateTime().tableName
// which can throw a NullPointerException on access.
try {

  if (obj === null || obj === undefined) {
    result.type = '' + obj;
    result.string = result.type;
    return result;
  }

  type = snd_Xplore.getType(obj);
  result.type = type.name;
} catch (ex) {
  try {
    result.type = typeof obj;
  } catch (ex2) {
    result.type = '__unknown__';
    result.string = '[Property access error: ' + ex2 + ']';
    return result;
  }
}

if (lists.warnlist.indexOf(',' + result.type + ',') > -1) {
  result.string = '*WARN LISTED*';
  return result;
}

if (options.show_strings === false) {
  result.string = '*IGNORED*';
} else {
  try {
    result.string = this.prettyPrinter.format(obj, type, options.use_json);
  } catch (e) {
    snd_Xplore.gswarn('Warning: unable to show pretty format for "' + name + '" due to ' + e +
      ' Showing string format instead.');
    result.string += this.prettyPrinter.String(obj);
  }
}

return result;
};

snd_Xplore._getPropertyArray = function (name) {
var result;
if (typeof window !== 'undefined') {
  result = [];
} else {
  result = gs.getProperty(name, '').toString();
  result = result ? result.split(',') : [];
}
return result;
};

/**
* Add elements for the Ignore List not captured by RegExp.
* These property names will be completely ignored.
*
* @param {String} item The name of a property name to completely ignore.
* @arguments Add further item names.
*/
snd_Xplore._ignorelist = snd_Xplore._getPropertyArray('snd_xplore.ignorelist');
snd_Xplore.ignorelist = function (item) {
if (arguments.length) {
  snd_Xplore._ignorelist = snd_Xplore._ignorelist.concat(Array.prototype.slice.apply(arguments));
  snd_Xplore._lists = null;
}
return snd_Xplore._ignorelist;
};

/**
* Add elements for the warn List which must not use toString as they will throw
* an exception. These property names will still show up in the results.
*
* @param {String} item The name of a property name to completely ignore.
* @arguments Add further item names.
*/
snd_Xplore._warnlist = snd_Xplore._getPropertyArray('snd_xplore.warnlist');
snd_Xplore.warnlist = function (item) {
if (arguments.length) {
  snd_Xplore._warnlist = snd_Xplore._warnlist.concat(Array.prototype.slice.apply(arguments));
  snd_Xplore._lists = null;
}
return snd_Xplore._warnlist;
};

/**
* Compile the ignore and warn lists into an object.
*
* @return {Object}
*     ignorelist {String}
*     warnlist {String}
*/
snd_Xplore.getLists = function () {
if (snd_Xplore._lists === null) {
  // prefix/suffix with comma so exact search can be made ',foo,'
  snd_Xplore._lists = {
    ignorelist: ',' + snd_Xplore._ignorelist.join(',') + ',',
    warnlist: ',' + snd_Xplore._warnlist.join(',') + ','
  };
}
return snd_Xplore._lists;
};

/**
* Navigate down the property chain of a given object.
*
* @param {Object} obj The object to navigate.
* @param {String} path The path to follow, e.g. "child.name". If any property
*     has a double parentheses, it will be called as a function.
* @return {Object} The property at the end of the chain.
* @throws {Error}
*/
snd_Xplore.dotwalk = function (obj, path) {
// summary:
//   Dotwalk a path on an object
var pathArr = path.split(".").reduce(function (r, c) {
  r.push(c.replace(/&#46;/g, "."));
  return r;
}, []);
var o = obj;
for (var i = 0; i < pathArr.length; i++) {
  path = pathArr[i];
  if (path.indexOf('()') > 0) {
    o = o[path.substr(0, path.length - 2)]();
  } else {
    try {
      o = o[path];
    } catch (ex) {
      throw new Error('Cannot dotwalk with a non-object: ' + pathArr.slice(0, i + 1).join('.'));
    }
  }
}
return o;
};

snd_Xplore.getPropertyNames = function getPropertyNames(obj) {
var type = snd_Xplore.getType(obj);
var parent_type;
var result;

// attempt to use getOwnPropertyNames in the first instance
if (!type.is_java && (obj instanceof Object || typeof obj === 'function')) {
  try {
    result = Object.getOwnPropertyNames(obj);
  } catch (e) {
    snd_console.gswarn('Error reading property names from object: ' + e);
    // do nothing - prevent 'not an object' errors with Java based objects
  }
}

// make sure we have an array
if (!result) {
  result = [];
}

// get everything else
for (var x in obj) {
  if (result.indexOf(x) === -1) result.push(x);
}

result.sort();
return result;
};

snd_Xplore.getType = function getType(obj) {
var type = {};
var match;

type.string = Object.prototype.toString.call(obj);
type.is_java = type.string.indexOf('Java') > -1;
type.name = type.string.slice(8, -1);

return snd_Xplore._getType(obj, type);
};

// This is in a separate function to prevent ClassCastException.
// For some reason, the presence of a try/catch in getType is throwing CCE. 
snd_Xplore._getType = function _getType(obj, type) {
try {
  type.of = typeof obj;
} catch (e) {
  if (e.message && e.message.indexOf('Invalid JavaScript value of type') > -1) {
    type.of = 'object';
    match = e.message.match(/(?:of type) (\S+)/);
    type.namespace = match ? match[1] : 'unknown';
  } else {
    snd_Xplore.gswarn('Error finding type in getType: ' + e);
  }
}
return type;
}

//==============================================================================
// Pretty Printer
//==============================================================================

snd_Xplore.PrettyPrinter = function () {
this.is_browser = typeof window !== 'undefined';
this.global = this.is_browser ? window : global;
this.scope =  (function () { return this; })();
this.not_str_regex = /^\[[a-zA-Z0-9_$. ]+\]$|^[a-zA-Z0-9.$]+@[a-z0-9]+$/;
};
snd_Xplore.PrettyPrinter.prototype = {

type: 'PrettyPrinter',

noQuotes: function (b) {
  this.no_quotes = !!b;
},

'String': function (obj) {
  try {
    obj = obj + '';
  } catch (e) {
    obj = Object.prototype.toString.call(obj);
  }

  // handle object types and memory references
  if (this.no_quotes || obj.match(this.not_str_regex)) {
    return obj;
  }

  return '"' + obj + '"';
},

'Boolean': function (obj) {
  return obj ? 'true' : 'false';
},

'Function': function (obj) {
  return Function.prototype.toString.call(obj);
},

'Number': function (obj) {
  return '' + obj;
},

'Array': function (obj) {
  // var str = [];
  // for (var i = 0; i < obj.length; i++) {
  //   str.push(this.format(obj[i]));
  // }
  // return '[' + str.join(', ') + ']';
  try {
    return JSON.stringify(obj, '', 2);
  } catch (e) {
    snd_Xplore.gswarn('Warning: unable to show JSON format due to ' + e +
      ' Showing string format instead.');
    return obj.toString();
  }
},

'SNRegExp': function (obj) {
  return obj.toString();
},

'GlideRecord': function (obj) {
  return [
    'table: ' + obj.getLabel() + ' [' + obj.getTableName() + ']',
    'sys_id: ' + obj.getUniqueValue(),
    'display: ' + obj.getDisplayValue(),
    'query: ' + obj.getEncodedQuery(),
    'link: ' + obj.getLink(true)
  ].join('\n');
},

'GlideElement': function (obj, type) {
  var internal_type = 'string',
      ed;

  if (type.indexOf('GlideElementHierarchicalVariables') > -1) {
    return ''; // prevent TypeError: Cannot find default value for object.
  }

  try {
    ed = obj.getED();
    internal_type = ed.getInternalType();
  } catch (e) {};

  if (internal_type == 'boolean') {
    return obj ? 'true' : 'false';
  }
  if (internal_type == 'reference') {
    return this.GlideElementReference(obj);
  }
  if (ed.isChoiceTable()) {
    return obj.getDisplayValue() + ' [' + obj + ']';
  }

  try {
    if (type.indexOf('Scoped') > -1) {
      obj = '' + obj;
    } else {
      obj = obj.getValue ? obj.getValue() : '' + obj;
    }
    return this.format(obj);
  } catch (ex) {
    return ex.toString();
  }
},

'GlideElementReference': function (obj) {
  var result = '"' + obj + '"';
  if (!obj.nil()) {
    result += ' [';
    if (obj.getReferenceTable) {
      result += obj.getReferenceTable() + ' ';
    }
    result += this.String(obj.getDisplayValue()) + ']';
  }
  return result;
},

format: function (obj, type, use_json) {
  var root_type = type.name.replace('GlideScoped', 'Glide');

  if (this.is_browser) {
    return type.name in this ? this[type.name](obj) : '' + obj;
  }

  if (obj === this.global || type.name == 'global') {
    return '[global scope]';
  }

  if (obj === this.scope) {
    return '[' + type + ' scope]';
  }

  if (root_type.indexOf('GlideRecord') > -1) return this.GlideRecord(obj);
  if (root_type.indexOf('GlideElement') > -1) return this.GlideElement(obj, type.name);

  // handle native JavaScript objects which would be useful to see as JSON
  if (use_json && (obj instanceof Object || Array.isArray(obj))) {
    return JSON.stringify(obj, '', 2);
  }

  // handle native JavaScript objects which we know have a toString
  if (obj instanceof Function ||
      obj instanceof Object ||
      obj instanceof Array ||
      type.name == 'Number' ||
      type.name == 'Boolean' ||
      type.name == 'String' ||
      obj instanceof RegExp) {
    return type.name in this ? this[type.name](obj) : this.String(obj);
  }

  // Java objects can have the same type but break when calling toString
  // We would only get here if their instanceof did not match.
  if (type.of == 'function' || type.name === 'Function' || type.name === 'Object') {

    try{
      return '' + obj;
    } catch (e) {
      if (e.message && e.message == 'Cannot find default value for object.') {
        return '';
      } else {
        snd_Xplore.gswarn('Error converting to string: ' + e);
      }
    }

    return '';
  }

  // catch all
  try {
    return this.String(obj);
  } catch (e) {
    return type.string;
  }
},

toString: function () {
  return '[object ' + this.type + ']';
}

};

//==============================================================================
// Default Print Reporter
//==============================================================================

/**
* Follow this object format in order to build a custom reporter that
* can be passed into snd_xplore for custom reporting requirements.
*/
snd_Xplore.PrintReporter = function () {
this._fn = '';
if (typeof window !== 'undefined') {
  this._fn = typeof console !== undefined && console.log ? 'console' : 'jslog';
} else {
  this._fn = 'print' in gs ? 'debug' : 'print';
}
};
snd_Xplore.PrintReporter.prototype.type = 'PrintReporter';
snd_Xplore.PrintReporter.prototype.toString = function () {
return '[object ' + this.type + ']';
};

snd_Xplore.PrintReporter.prototype.print = function (str) {
if (this._fn == 'console') console.log(str);
else if (this._fn == 'jslog') jslog(str);
else if (this._fn) gs[this._fn](str);
};

/**
* Called when the main object is evaluated.
* @param {Object} obj A result object
*/
snd_Xplore.PrintReporter.prototype.begin = function (result) {
this.print('Xplore: [' + result.type + '] : ' + result.string);
};

/**
* Called each time a property of the object is evaluated.
* @param {Object} result A result object
*/
snd_Xplore.PrintReporter.prototype.result = function (result) {
this.print('[' + result.type + '] ' + result.name + ' = ' + result.string);
};

/**
* Called when snd_xplore has finished running.
*/
snd_Xplore.PrintReporter.prototype.complete = function () {
this.print('Complete.');
};

//==============================================================================
// Object Reporter
//==============================================================================

/**
* Pushes an array of objects containing two properties describing the message
* into the response.messages array.
*
* Must be run in global scope.
*
* @summary Get all the output messages generated in this session, then flush them.
* @return {Array} An array of objects where each object in the array contains:
*     type {String} The property type: log, info, error, access
*     message {String} The message.
*
**/
snd_Xplore.getOutputMessages = function () {

function add(type, message) {
  var o = {};
  //o.date = new Date().getTime();
  o.type = type;
  o.message = message;
  o.is_json = snd_Xplore.isJson(message);
  ret.push(o);
}

var ret = [],
    tmp,
    i;

if (typeof window !== 'undefined') return ret;

// access
tmp = gs.getAccessMessages().toArray();
for (i = 0; i < tmp.length; i++) {
  add('access', tmp[i]);
}

// errors
tmp = gs.getErrorMessages().toArray();
for (i = 0; i < tmp.length; i++) {
  add('error', tmp[i]);
}

// info
tmp = gs.getInfoMessages().toArray();
for (i = 0; i < tmp.length; i++) {
  add('info', tmp[i]);
}

var logs;
if (snd_Xplore._logtail) {
  logs = [].concat(this._gslogs, snd_Xplore._logtail.getMessages());
  logs.sort(function (a, b) {
    if (a.date > b.date) return 1;
    if (b.date > a.date) return -1;
    return 0;
  });
  ret = ret.concat(logs);
} else {

  // merge gslog workaround for Istanbul onwards
  ret = ret.concat(snd_Xplore._gslogs); 
}


// gs.print
//   tmp = GlideSessionDebug.getOutputMessages().toArray();
//   try {
//     for (i = 0; i < tmp.length; i++) {
//       add('log', ('' + tmp[i].line).replace(' : ', ' ')); // remove unnecessary colon
//     }
//   } catch (e) {
//     if (tmp.length) {
//       ret.unshift({type: 'access', message: '<p>Hey!<p>' +
//         '<p>It looks like you\'re using <code>gs.print</code>, <code>gs.info</code>, ' +
//         '<code>gs.warn</code> or <code>gs.error</code> in your script.</p>' +
//         '<p>Unfortunately ServiceNow have locked down the API and we no longer have access to read ' +
//         'those messages. You can see them by going to Logs > Node Logs; the time and thread name ' +
//         'should be pre-populated for this thread. Alternatively you can replace those methods with ' +
//         '<code>gs.addInfoMessage</code> although I appreciate this doesn\'t work for Script Includes, etc.</p>' +
//         '<p>If you have any insight or can help fix this, please get in touch!</p>' +
//         '<p>Thanks! James</p>' +
//         '<p><small>Original exception: ' + e.toString() + '</small></p>'});
//     }
//   } finally {
//     // remove all the messages we just retrieved
//     GlideSessionDebug.clearOutputMessages();
//   }

gs.flushAccessMessages();
gs.flushMessages();

for (var i = 0, msg; i < ret.length; i++) {
  msg = ret[i];
  msg.message = snd_Xplore.formatDate(msg.date) + (msg.is_json ? '\n' : ' ') + msg.message;
}

return ret;
};


/**
* Server side method for getting any errors or warning that occured for the
* user in the last minute or so. Logs are pushed to the main result object.
*
* @return {Array} An array of objects containing the properties:
*     created {Number]
*     level {String}
*     message {String}
*     source {String}
**/
snd_Xplore.getLogs = function () {
var ret = [],
    level_map,
    gr;

if (typeof window !== 'undefined') return ret;

level_map = {
  '-1': 'Debug',
  '0': 'Info',
  '1': 'Warning',
  '2': 'Error'
};

gr = new GlideRecord('syslog');
gr.addQuery('sys_created_on', 'ON', 'Current minute@javascript:gs.minutesAgoStart(0)@javascript:gs.minutesAgoEnd(0)');
gr.addQuery('sys_created_by', '=', gs.getUserName())
    .addOrCondition('sys_created_by', '=', 'system');
gr.addQuery('level', 'IN', '1,2');
gr.orderBy('sys_created_on');
gr.query();

while (gr.next()) {
  ret.push({
    created: gr.sys_created_on.getDisplayValue(),
    level: level_map[gr.getValue('level')],
    message: gr.getValue('message'),
    source: gr.getValue('source')
  });
}

return ret;
};

/**
* A reporter to pass to snd_xplore for capturing results so they can be shown
* in the UI.
*/
snd_Xplore.ObjectReporter = function () {
this.is_browser = typeof window !== 'undefined';
this.start_time = this.is_browser ? new Date() : new GlideDateTime().getDisplayValue();
this.report = {
  // The type of object that is being evaluated
  type: '',
  // the valueOf value of the object being evaluated
  value: '',
  // the string value of the object being evaluated
  string: '',
  // the list of property objects
  results: [],
  // the list of captured messages
  messages: [],
  // a list of logs that occured very recently
  logs: [],
  // self explanatory really!
  status: '',
  // the url to access the node logs
  node_log_url: ''
};
};
snd_Xplore.ObjectReporter.prototype.type = 'ObjectReporter';
snd_Xplore.ObjectReporter.prototype.toString = function () {
return '[object ' + this.type + ']';
};

snd_Xplore.ObjectReporter.prototype.getReport = function () {
return this.report;
};

snd_Xplore.ObjectReporter.prototype.begin = function (obj) {
this.report.status = 'started';
this.report.type = obj.type;
this.report.value = obj.value;
this.report.string = obj.string;
};

snd_Xplore.ObjectReporter.prototype.result = function (obj) {
this.report.results.push(obj);
};

snd_Xplore.ObjectReporter.prototype.complete = function () {
this.report.status = 'finished';
this.report.start_time = this.start_time;
this.report.end_time = this.is_browser ? new Date() : new GlideDateTime().getDisplayValue();
this.report.node_log_url = this.generateNodeLogUrl();
};

snd_Xplore.ObjectReporter.prototype.generateNodeLogUrl = function () {
// create the URL for the ui_page to display the log data
var maxRows = 2000;
var url = "ui_page_process.do?name=log_file_browser";

if (this.is_browser) return '';

url += "&end_time=" + (new GlideDateTime().getDisplayValue());
url += "&start_time=" + this.start_time;
url += "&max_rows=" + maxRows;
url += '&filter_thread=' + this.getThreadName();
return url;
};
snd_Xplore.ObjectReporter.prototype.getThreadName = function () {
// this works around the exceptions thrown by calling getName() or using .name
// Those are totally broken in Jakarta.
var value = '' + Object.prototype.valueOf.call(GlideWorkerThread.currentThread());
var m = value.match(/(?:\[)([^,]+)/);
if (!m) {
  snd_Xplore.gswarn('Cannot get current thread name.', 'snd_Xplore');
  return '';
}
return m[1];
};

snd_Xplore.getVersion = function () {
var gr = new GlideRecord('sys_app');
gr.addQuery('sys_id', '=', '0f6ab99a0f36060094f3c09ce1050ee8');
gr.setLimit(1);
gr.query();
return gr.next() ? gr.getValue('version') : 'Unknown';
};

///////////////////////////////////////////////////////
// Workaround for log statements in Istanbul onwards //
///////////////////////////////////////////////////////

snd_Xplore._gslogs = [];
snd_Xplore._gslogMessage = function (level, msg, source, params) {
//var time = new Date().toISOString().replace('T', ' ').replace(/\.(\d+)Z/, ' ($1)');
var tmp = {};
tmp.date = new Date().getTime();
tmp.type = level;
tmp.source = source;
if (typeof msg === 'string') {
if (Array.isArray(params)) {
  msg = snd_Xplore.substitute(msg, params);
} else {
  tmp.is_json = snd_Xplore.isJson(msg);
}
} else {
  tmp.is_json = true;
  msg = JSON.stringify(msg, null, 2);
}
//tmp.message = time + ': ' + (tmp.is_json ? '\n' : '') + msg;
tmp.message = msg;
snd_Xplore._gslogs.push(tmp);
};
snd_Xplore.gsprint = function (msg) {
snd_Xplore._gslogMessage('-1', msg);
};
snd_Xplore.gslog = function (msg, source) {
snd_Xplore._gslogMessage('-1', msg, source);
};
snd_Xplore.gsdebug = function (msg, p1, p2, p3, p4, p5) {
snd_Xplore._gslogMessage('-1', msg, null, [p1, p2, p3, p4, p5]);
};
snd_Xplore.gsinfo = function (msg, p1, p2, p3, p4, p5) {
snd_Xplore._gslogMessage('0', msg, null, [p1, p2, p3, p4, p5]);
};
snd_Xplore.gswarn = function (msg, p1, p2, p3, p4, p5) {
snd_Xplore._gslogMessage('1', msg, null, [p1, p2, p3, p4, p5]);
};
snd_Xplore.gserror = function (msg, p1, p2, p3, p4, p5) {
var params;
if (p1 && p1 instanceof Error) {
  msg += ': ' + p1;
} else {
  params = [p1, p2, p3, p4, p5];
}
snd_Xplore._gslogMessage('2', msg, null, params);
};
snd_Xplore.notice = function (msg) {
var tmp = {};
tmp.type = '1'; // warn
tmp.message = 'Notice: ' + msg;
snd_Xplore._gslogs.push(tmp);
};
snd_Xplore.substitute = function substitute(msg, params) {
return msg.replace(/{(\d)}/g, function (m, index) {
  return params.length > index ? params[index] : m;
});
};

snd_Xplore.isJson = function isJson(str) {
try {
  JSON.parse(str);
} catch (e) {
  return false;
}
return true;
};

snd_Xplore.formatDate = function formatDate(date) {
if (!date) return '';
var gdt = new GlideDateTime();
var ms = ('00' + (new Date(date).getMilliseconds())).slice(-3);
gdt.setNumericValue(date);
return gdt.getDisplayValue() + ' (' + ms  + ')';
};

//==============================================================================
// Xplore Logtail
//==============================================================================

/**
* Class for capturing logs. Does not capture scoped logs, only global.
*
*/
snd_Xplore.Logtail = function Logtail() {
this.channel_name = 'logtail';
this.cm = GlideChannelManager.get();
if (!this.cm.exists(this.channel_name)) {
   throw 'Channel does not exist: ' + this.channel_name;
}
this.channel = this.cm.getChannel(this.channel_name);

// Copy ChannelAjax
GlideThreadAttributes.createThreadAttribute("streaming_channel", this.channel_name);

this.start_sequence = this.channel.getLastSequence();

// we need this here (after getting the last channel sequence) so we can determine the transaction ID later
gs.debug('Starting ' + this.type);

this.messages = [];

this.filter_messages = true;

};

snd_Xplore.Logtail.prototype.type = 'snd_Xplore.Logtail';

/**
* Call this before executing any code to capture logs.
*
*/
snd_Xplore.Logtail.start = function start() {
// give to main object for getOutputMessages()
snd_Xplore._logtail = new snd_Xplore.Logtail();
return snd_Xplore._logtail;
};

snd_Xplore.Logtail.getMessages = function getMessages() {
if (snd_Xplore._logtail) {
  return snd_Xplore._logtail.getMessages();
}
};

snd_Xplore.Logtail.prototype._messageFactory = function _messageFactory(date, message) {
var type = 'log';
if (message.indexOf('no thrown error') > -1) type = 'error'; // find call to gs.error
var o = {};
o.type = type;
o.message = message;
o.date = date;
o.is_json = snd_Xplore.isJson(message);
return o;
};

snd_Xplore.Logtail.prototype._getTransactionId = function _getTransactionId(message) {
return message.substr(0, 32);
};

snd_Xplore.Logtail.prototype.validateMessage = function validateMessage(channel_message) {
var message = channel_message.getMessage();
var ignore = false;

if (!this.transaction_id) {
  if (message.indexOf(this.type) === -1) {
    if (this.filter_messages) return false;
  } else {
    this.transaction_id = this._getTransactionId(message);
    // this.messages.push(this._messageFactory('Transaction ID: ' + this.transaction_id));
    if (this.filter_messages) return false; // the user doesn't need to see this message really
  }
} else {
  if (this.filter_messages && this.transaction_id != this._getTransactionId(message)) {
    return false;
  }
}

if (this.filter_messages) {
  message = message.substr(33); // remove Transaction ID
  if (message.indexOf('*** Script: ') === 0) {
    message = message.substr(12);
  }
}

return this._messageFactory(channel_message.getDate(), message);
};

snd_Xplore.Logtail.prototype.getMessages = function getMessages() {
var channel_messages = this.channel.getMessages();
var size = channel_messages.size();
var transaction;
this.messages = [];
for (var i = 0; i < size; i++) {
  var message = channel_messages.get(i);
  if (message.getSequence() > this.start_sequence) {
    message = this.validateMessage(message);
    if (message) {
      this.messages.push(message);
    }
  }
}
return this.messages;
};

snd_Xplore.ScriptHistory = (function () {
function ScriptHistory() {}

ScriptHistory.prototype.type = 'snd_Xplore.ScriptHistory';

ScriptHistory.SCRIPT_PREFIX = 'xplore.script.';

ScriptHistory.HISTORY_PREFIX = 'xplore.history.';

ScriptHistory.prototype.store = function store(options) {
  var result;

  if (!options || typeof options != 'object') {
    throw new Error('Invalid options object.');
  }

  options = Object.extend({}, options);

  // If we are running a script again that was loaded, then let's just update it
  // to match the current settings.
  // If the script has changed (or was not loaded) then we'll create a new entry.
  if (options.loaded_id) {
    this._preventDuplicate(options);
  }

  options.id = options.id || (ScriptHistory.HISTORY_PREFIX + new Date().getTime());
  options.name = options.name || new GlideDateTime().getDisplayValue() + ' Script';

  if (!options.id.startsWith(ScriptHistory.HISTORY_PREFIX)) {
    options.id = ScriptHistory.HISTORY_PREFIX + options.id;
  }

  result = this.setPreference(options.id, JSON.stringify(options), options.name);

  this.enforceHistoryLimit(gs.getProperty('snd_xplore.history.limit'));

  return result;
};

ScriptHistory.prototype._preventDuplicate = function (options) {
  var script = this.get(options.loaded_id);
  var diff = false;
  if (!script) return;

  'target,scope,code,user_data,user_data_type,support_hoisting'.split(',').forEach(function (name) {
    diff = diff || (options[name] != script[name]);
  });

  if (diff) return;

  // Use the historic script ID to prevent the same script being inserted numerous times
  options.id = options.loaded_id;
}

ScriptHistory.prototype.get = function get(id) {
  var preference = this.getPreference(id);
  if (!preference) return;
  try {
    return JSON.parse(preference.getValue('value'));
  } catch (e) {
    return {
      id: preference.getValue('name'),
      name: '[Invalid script]'
    };
  }
};

ScriptHistory.prototype.retrieve = function retrieve() {
  var result = [];
  var obj,
      gr;

  gr = new GlideRecord('sys_user_preference');
  gr.addQuery('user', '=', gs.getUserID());
  gr.addQuery('name', 'STARTSWITH', ScriptHistory.SCRIPT_PREFIX)
    .addOrCondition('name', 'STARTSWITH', ScriptHistory.HISTORY_PREFIX);
  gr.orderByDesc('sys_updated_on');
  gr.query();

  while (gr.next()) {
    try {
      result.push(JSON.parse(gr.getValue('value')));
    } catch (e) {
      result.push({
        id: gr.getValue('name'),
        name: '[Invalid script]'
      });
    }
  }

  return result;
};

ScriptHistory.prototype.renameScript = function renameScript(id, name) {
  var gr = this.getPreference(id);
  var obj;
  if (gr) {
    try {
      obj = JSON.parse(gr.getValue('value'));
    } catch (e) {
      throw new Error('Unable to rename script because of corrupt database value.');
    }
    obj.name = name;
    gr.setValue('description', name);
    gr.setValue('value', JSON.stringify(obj));
    return gr.update();
  }
  return false;
};

ScriptHistory.prototype.remove = function remove(id) {
  var gr = this.getPreference(id);
  if (gr) {
    return gr.deleteRecord();
  }
  return false;
};

ScriptHistory.prototype.enforceHistoryLimit = function enforceHistoryLimit(max_count, delete_limit) {
  max_count = max_count || 50;

  // we should only need to delete one - one in, one out, but this covers it somewhat if the settings are changed
  delete_limit = delete_limit || 50;
  var count;
  var gr = new GlideRecord('sys_user_preference');
  gr.addQuery('user', '=', gs.getUserID());
  gr.addQuery('name', 'STARTSWITH', ScriptHistory.HISTORY_PREFIX);
  gr.orderByDesc('sys_updated_on');
  gr.chooseWindow(max_count, delete_limit);
  gr.query();

  count = gr.getRowCount();
  if (count > (max_count + delete_limit)) {
    snd_Xplore.gsError('Aborting snd_Xplore.ScriptHistory.enforceHistoryLimit() to prevent ' +
      'unexpected data loss. Found ' + count + ' which breached the safety limit of ' +
      (max_count + delete_limit) + '. Query used: ' + gr.getEncodedQuery());
    return;
  }

  while (gr.next()) {
    gr.deleteRecord();
  }
};

ScriptHistory.prototype.setPreference = function setPreference(name, value, description) {
  var gr = this.getPreference(name);

  if (!gr) {
    gr = new GlideRecord('sys_user_preference');
    gr.user = gs.getUserID();
    gr.name = name;
  }

  gr.value = value;
  gr.description = description;
  return gr.update();
};

ScriptHistory.prototype.getPreference = function getPreference(name) {
  var gr = new GlideRecord('sys_user_preference');
  gr.addQuery('name', '=', name);
  gr.setLimit(1);
  gr.query();
  return gr.next() ? gr : null;
};

return ScriptHistory;
})();

Sys ID

04cf1a2f0fd2020094f3c09ce1050eba

Offical Documentation

Official Docs: