javascript

Standardisera återanvändning av JavaScript i ServiceNow

Det blir normalt sett en del scriptande i ServiceNow. Smart grej är att tänkta på att designa koden så den går att återanvända. Då slipper man själv, och kollegorna, att utveckla samma funktion flera gånger och man spar tid. Man måste så klart ha ett arbetssätt så kollegorna vet om vad det finns för funktion att tillgå utan att man måste hålla på med för mycket dokumentering. Har man ett standardiserat sätt att jobba med återanvändning så ska det vara lätt att hitta funktion som har blivit utvecklad i systemet.

Allt i denna artikel är gjort i Orlando.

Naturliga ställen för att skapa kod för återanvändning är i Script include på serversidan och UI Script på klientsidan. Vi skapar klasser i vilka vi kan ha funktioner som vi når från övriga script på server- respektive klientsidan. Det är praktiskt om vi kan använda oss av abstrakta metoder så vi slipper skapa objekt innan användning. Det kan gå, men inte alltid. Exempelvis måste vi skapa objekt om vi vill att objektet ska ha kunna hålla på värden mellan anrop till klassens funktioner.

Det var ju enkelt, då gör vi så. Var det allt? Nja, vi kanske måste kolla på hur vi dokumenterar detta. Bra kod dokumenterar sig själv säger man och man räknar med kommentarer i texten till det så är det helt sant. Helst skulle man ju vilja generera JSDoc från koden man gjort men det tittar vi inte på nu. Sen måste vi titta lite på hur vi skapar klasserna också. Vill vi ha abstrakta metoder? Inte lika rättframt som för Java även om det är besläktade språk.

Namnstandard

För att vara säkra på att de klasser vi skapar inte kolliderar med de klasser som redan finns i ServiceNow behöver vi en namnstandard.

<prefix><objekt>Utils

Prefix är 1-2 bokstäver som gör att klassen skiljer sig om vi råka skapa en klass för ett objekt där ServiceNow redan har skapat en klass.

Objekt är den, troligtvis, tabell som ska användas. Kan vara andra entiteter också.

Utils är bara en statisk text som visar att det är någon form av verktygs- eller hjälpklass.

Exempel:

scIncidentUtils

‘sc’ står i det här fallet för SMICloud (mitt företag). ‘incident’ för att klassen ska användas i incidentsammanhang. Hade vi inte lagt till ‘sc’ i början hade vi fått krock med incidentUtils, på serversidan, som kommer med ServiceNow.

Klass

Vi skapar klasser i UI Script för klientsidan och Script Includes för serversidan. Vi jobbar med klasser för det är välstrukturerat sätt att kapsla in funktioner relaterade till samma område. Vi kommer kunna anropa en funktion genom att antingen anropa den direkt via klassen (abstract) eller skapa ett objekt och använda det för att anropa funktioner. Här kommer du behöva ta ett beslut, behöver du att klassen bär på data som du ska använda senare i scriptet måste du skapa ett objekt. Behöver du använda en funktion bara för att göra ett anrop och sen behöver du inte klassen längre, du kan du anropa funktionen utan att skapa ett objekt. Kallas abstrakt funktion normalt sett (oklart om det är rätt term i det här fallet, men sättet är iaf likt).

Exempel

Låt säga att en incident inte ska kunna relatera till ett problem som har lägre prioritet än själva incidenten. Vi gör jämförelsen när vi försöker spara när incidenten fått ett nytt relaterat problem eller när incidentens prioritet ändras. Är problemets prioritet lägre än incidentens så ska inte uppdateringen sparas och ett meddelande skrivas i formuläret. I en komplett lösning måste man även implementera kontroller på problemsidan men vi utgår bara från incidentsidan i detta exempel.

Vi skapar först ett Script include som göra själva jämförelsen (Jag har inte använt mig av fellhanteringen jag pratar om här för att göra koden så ren som möjligt. Lägg gärna till den i skarpa fall.):

/** @class      scIncidentUtil      Utility class incidents */
var scIncidentUtil = Class.create();
scIncidentUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    /** Compare two objects' priorities. The both input parameters/objects must to have a field 'priority' of the same format. 
    *@param     reference1      The first reference object that the priority should be compared with.  
    *                       reference2      The second reference object that the priority should be compared with. 
    **/
    comparePriorities: function(reference1, reference2) {
            //Compare using normal javascript functionality and return. Exactly what should happen is not decided here since that would minimize the general use of this function
            return reference1.priority.localeCompare(reference2.priority);

    },

    type: 'scIncidentUtil'
});

Notera att denna Script include är det API vi skapat och i vilken vi ska fylla på med funktioner relaterat till incidenter. Skapa en annan Script include för övriga tabeller/objekt.

Vi skapar sen en Business rule från vilken vi använder vår util-klass och funktion. Sätt följande värden:

  • Markera Advanced då vi måste scripta.

  • When sätts till before (om vi måste göra ‘abort’ så får inget sparas innan.

  • Business rule används när vi skapar och uppdaterar incidenten så markera Insert och Update.

  • Kör regeln endast om man väljer nytt relaterat problem till incidenten eller när incidentens prioritet uppdateras. Sätts i Filter Condition.

Business rule

Business rule


Och så klipper vi in detta scrip under flik Advanced:

(function executeRule(current, previous /*null when async*/) {

    //In case problem has a lower priority than the incident, abort and inform the user.
    var comparison = scIncidentUtil.prototype.comparePriorities(current, current.problem_id);
    if(comparison == -1){
        gs.addErrorMessage("Incident cannot have higher priority than related problem. Update not saved.");
    }
    
})(current, previous);

När vi nu uppdaterar incidentens prioritet eller relaterade problem görs kontrollen. Om incidentens prioritet är lägre eller lika med problemets prioritet så händer inget. Annars skrivs ett meddelande ut i formuläret:

Felmeddelande i incidentformuläret

Felmeddelande i incidentformuläret

Ett alternativ till ovan lösning hade kunna vara att lägga den på task-nivå. Då hade man kunnat använda den för objekt som ärver task-tabellen också. I detta scenario så sker jämförelsen på ett lite annat sätt på problemsidan så kanske är det bra att skapa en utility-klass för problem där man itererar igenom alla relaterade incidenter och gör en jämförelse. Ingenting hindrar dig från att göra själva iterationen i en utility-klass för problem och göra de enskilda jämförelserna m.h.a. denna utility-klass för incident (även om det kanske är lite overkill för en sådan enkel jämförelse).

Fortsätt läsa

Felhantering av script i ServiceNow

För långsiktig, och varför inte kortsiktig, stabilitet i ServiceNow bör man ha en bra felhantering i scripten man utvecklar. Här beskriver jag hur man kan skapa ett standardiserat sätt för detta i plattformen för att inte behöva återskapa hjulet hela tiden samt för att alla ska ha samma sätt att arbeta på.

Allt i denna artikel är gjort i Orlando.

Lösningen består av följande:

  • En Script Include

  • Ett UI Script

  • Ett Scheduled Script

Exempel för användning från både klientsida och serversida är beskrivna nedan.

I Script Include och UI Script finns javaklasser/API definierade som används för att på ett standardiserat sätt logga fel som uppstår i script man utvecklat. Denna loggning hamnar i error logg (‘syslog’).

För att man inte ska behöva gå in och titta varje dag om det kommit några nya fel så skapar man ett schemalagt script som kollar om det dykt upp några nya fel. Har det gjort det skapas en incident med information om de fel som uppstått och läggs på rätt CI och rätt grupp.

Lösningen är gjord på det sätt att det inte är någon skillnad på hur man använder API:t på klient eller serversidan.

Format på information i error log

Outputen i loggen kan exempelvis se ut så här:

[ERROR][Error type: RangeError][Script type: Client Script][Script: Change Assigned To][Function: onChange][Other info: n/a][Thrown error message: toPrecision() argument must be between 1 and 100]

Beskrivning i den ordning de loggas:

[<nivå>] 		[ERROR] 		 

INFO, WARNING, ERROR | Notera att man anger manuellt detta värde i koden och det sker ingen validering.

[<feltyp>] 		[RangeError]

Här skrivs felet in som man hämtar från objekt “Error” som kastas av systemet.

[<scripttyp>] 		[Client Script]

Vilken typ av script det är så man lätt ska hitta det i systemet

[<namn>] 		[Change Assigned To]

Namnet på det script, affärsregel, etc. så man lätt ska hitta var felet kastades.

[<funktionsnamn>] 	[onChange]

Namnet på den funktion i vilken felet kastades.

[<övrig info>] 	[xxx]

Övrig information som man vill logga hamnar i det här fältet. Detta används troligtvis om man har fel som är återkommande och man behöver mer information vid debugging.

[<info om fel>] 	[Thrown error message: toPrecision() argument must be between 1 and 100]

Felmeddelande som systemet skickar med i det felobjekt som kastas.

Script Include

Först börjar vi titta på den Script Include som anropas från något script på serversidan eller från lösningens UI Script.

Som du ser i bilden nedan så jobbar vi i globala scopet då detta är något som ska kunna användas i hela plattformen. Den måste också vara anropbar från klienten.

Script Include scLogginUtil

Script Include scLogginUtil

Koden nedan kan du kopiera in i en script include som du döper till “scLoggingUtil”. I övrigt har jag dokumenterat vad som händer i koden i scriptet.

Vill du läsa mer om script includes och GlideAjax kan du läs här:

Script Include

GlideAjax

Script Include scLoggingUtil:

/** @class      scLogUtil       Utility class for handling logging */
var scLoggingUtil = Class.create();
scLoggingUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    /** Handles logging requests from the client*/
    logClientError: function() {
        try {
            
            //We need the data from the client to be put in the same ojbect structure as if the data was coming from a server side script and also mapped to some of the data coming with a thrown error (e.g. .name and .message). Do note that since a thrown error is an object we use object here instead of array.
            var parameters = {};
            parameters.name = this.getParameter('sysparm_error_name');
            parameters.level = this.getParameter('sysparm_level');
            parameters.typeOfScript = this.getParameter('sysparm_type_of_script');
            parameters.nameOfScript = this.getParameter('sysparm_name_of_script');
            parameters.nameOfFunction = this.getParameter('sysparm_name_of_function');
            parameters.otherInfo = this.getParameter('sysparm_other_info');
            parameters.message = this.getParameter('sysparm_message');

            this.logError(parameters);

            return true;

        } catch (err) { //In the unlikely event that something goes wrong in logClitenError(), we do a print to the error log directly from here

            gs.logError("[ERROR][" + err.name + "][Script include][scLoggingUtil][logClientError][" + err.message + "]");
            return false;

        }
    },

    /**
     *Print the error to the error log
     *@param        err     An object containing a defined set of information about the error. 
     *                      err.level: INFO, WARNING, ERROR. 
     *                      err.name: Type of error caught by a thrown error or defined manually. Preferable the first on if possible. 
     *                      err.typeOfScript: Client Script, Script Include, Business Rule, etc. 
     *                      err.nameOfScript: The actually name of the entitey where the script retains. 
     *                      err.nameOfFunction: A script can contain multiple functions. This defines the function where the error was thrown. 
     *                      err.otherInfo: In case there is a need, mainly for debuggin purpose, to post additional information to the error log, this information can be added to otherInfo.
     *                      err.message: The message thrown by the error. 
     */
    logError: function(err) {

        //The actually writing to the error log
        gs.logError("[" + err.level + "]" + 
                    "[Error type: " + err.name + "]" + 
                    "[Script type: " + err.typeOfScript + "]" + 
                    "[Script: " + err.nameOfScript + "]" + 
                    "[Function: " + err.nameOfFunction + "]" +
                    "[Other info: " + err.otherInfo + "]" + 
                    "[Thrown error message: " + err.message + "]");

    },

    type: 'scLoggingUtil'

});

UI Script

För att kunna använda denna funktion från klienten måste vi också ha ett UI Script som anropar ovan Script Include. Klienten anropar funktion logClientError() (om man anropar från script på serversida anropar man logError() direkt). Detta är inbakat i API:t och inget vi egentligen behöver tänka på när vi använder API:t.

UI Script ska vara globalt då det ska kunna nås från hela plattformen. I detta fall har jag bara brytt mig om Desktop som UI Type.

UI Script scLoggingUtil

UI Script scLoggingUtil

Koden nedan kan du kopiera in i ett UI Script som du döper till “scLoggingUtil”. I övrigt har jag dokumenterat vad som händer i koden i scriptet.

Vill du läsa mer om UI Script kan du gör det här:

UI Script

/** @class      scLogUtil       Utility class for handling logging */
var scLoggingUtil = Class.create();

scLoggingUtil.prototype = {

    initialize: function(){

    },

    /**Prepares the error data and sends it to the server
    *@param             level           INFO, WARNING, ERROR
    *@param             err             Thrown error object
    *@param             typeOfScript    The type of script where the error was thrown
    *@param             nameOfScript    The name of the script where the error was thrown
    *@param             nameOfFunction  Name of the function where the error was thrown 
    *@param             otherInfo       If additional information is needed, mainly for debuggin purpose, that information can be added here. 
    *
    */
    logError: function(err, level, typeOfScript, nameOfScript, nameOfFunction, otherInfo) {
        try{
            var ga = new GlideAjax('scLoggingUtil');        
            ga.addParam('sysparm_name', 'logClientError');
            ga.addParam('sysparm_message', err.message);
            ga.addParam('sysparm_error_name', err.name);
            ga.addParam('sysparm_type_of_script', typeOfScript);
            ga.addParam('sysparm_name_of_script', nameOfScript);
            ga.addParam('sysparm_name_of_function', nameOfFunction);
            ga.addParam('sysparm_other_info', otherInfo);

            ga.addParam('sysparm_level', level);
            ga.getXML(responseHandler);

        }catch(err){//In the unlikely event that something goes wrong in above function, we do a print to the concole  directly from here

            jslog("[ERROR]" + 
                  "[Error type: " + err.name + "]" + 
                  "[Script type: UI Script]" + 
                  "[Script: scLoggingUtil]" + 
                  "[Function: logError->catch]" +
                  "[Thrown error message: " + err.message + "]");

        }
        /**This callback function handles the response from the server. If everyhting works fine we do nothing. If something goes wrong on the server side (and answer is false) we print an error message to the console. 
        */
        function responseHandler(response) {
            var answer = response.responseXML.documentElement.getAttribute("answer");

            //Do note that the result is returned as a string, not a bool
            if(answer == "false"){
                jslog("[ERROR]" + 
                      "[Error type: Problem logging error in error log]" +
                      "[Script type: UI Script]" +
                      "[Script: scLogingUtil]" +
                      "[Function: logError->responseHandler]");
            }
        }
    },

    type: 'scLoggingUtil '


};

Användning från klientsidan

För att anropa scLoggingUtil från ett, exempelvis, Client Script gör man så här. “err” är det kastade felobjektet: :

var logUtil = new scLoggingUtil();      
logUtil.logError(err, "ERROR", "Client Script","Change Assigned To", "onChange", "n/a");

Du kan också anropar funktionen direkt:

scLoggingUtil.prototype.logError(err, "ERROR", "Client Script","Change Assigned To", "onChange", "n/a");


Se beskrivningen av funktion logError i UI Script scLoggingError ovan.

Om du har skapat både UI Script och Script Include enligt ovan kan du testa funktionen genom följande exempel:

1. Skapa ett Client Script för incident.

2. Sätt Type = onChange

3. Sätt Field name till Assigned to

4. Se till att den är Global

5. Kopiera in följande script och spara:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading || newValue === '') {
        return;
    }

    try{
        //As an example, we trigger an error on purpose. toPrecision() cannot be 500. 
        var num = 1;
        num.toPrecision(500); 
        
    }catch(err){
        //Let's catch the error and log it
        var logUtil = new scLoggingUtil();
        logUtil.logError(err, "ERROR", "Client Script","Change Assigned To", "onChange", "n/a");

    }
}

6. Gå till en existerande incident (om den redan är öppen ladda om så att scripten laddas).

7. Ändra Assigend To. Du behöver inte spara.

8. Gå till Error Log (“Errors” i menyn).

9. Sortera så senaste “Created” är högst upp. Nu bör du se något som detta i loggen:

[ERROR][Error type: RangeError][Script type: Client Script][Script: Change Assigned To][Function: onChange][Other info: n/a][Thrown error message: toPrecision() argument must be between 1 and 100]

Användning från serversidan

För att anropa Script Include scLoggingUtil från, exempelvis, en Business Rule gör man så här. “err” är det kastade felobjektet:

var logUtil = new scLoggingUtil();
logUtil.logError(err);

Du kan också anropa funktionen direkt:

scLoggingUtil.prototype.logError(err);

Se beskrivningen av funktion logError i Script Include scLoggingError ovan.

Om du har skapat Script Include enligt ovan kan du testa funktionen genom följande exempel:

1. Skapa en Business Rule för incident. D.v.s. sätt Table = Incident

2. Välj “When” till “Before”

3. Markera endast “Update”

4. Markera “Advanced”

5. Kopiera in följande script:

(function executeRule(current, previous /*null when async*/ ) {

    try {
        //As an example, we trigger an error on purpose. toPrecision() cannot be 500. 
        var num = 1;
        num.toPrecision(500);

    } catch (err) {

        //Prepare the data to be logged
        err.typeOfScript = "Business Rule";
        err.nameOfScript = "convertNum";
        err.level = "ERROR";
        err.nameOfFunction = "executeRule";
        err.otherInfo = "n/a";

        //Access script include scLoggingUtil and log the error
        var logUtil = new scLoggingUtil();
        logUtil.logError(err);


    }

})(current, previous);

6. Öppna eller gå till incidenten du ska testa med.

7. Ändra något och spara. Ändra inte Assigned To om du inte vill trigga ytterligare fel (om du har gjort klientövningen ovan).

8. Gå till Error Log (“Errors” i menyn).

9. Sortera så senaste “Created” är högst upp. Nu bör du se något som detta:

[ERROR][Error type: RangeError][Script type: Business Rule][Script: convertNum][Function: executeRule][Other info: n/a][Thrown error message: Precision 500 out of range.]

Samla ihop felen till en incident m.h.a. Scheduled Script

Det är inte till så stor nytta att ha ett gemensamt sätt att jobba med felhantering om inte felen upptäcks och kan hanteras. Därför är det bra med ett automatiskt jobb som kollar om det dykt upp några nya fel senaste dygnet och skapar en incident med information om felen. Notera att vi skapar max en incident per dygn men alla fel listas i den incidenten. Tänk om något fel uppstår flera hundra gången under ett dygn, jag tycker inte vi behöver ha flera hundra incidenter för det felet då. Antal gånger som felet uppstått kommer ändå stå i incidenten.

1 Skapa ett Scheduled Script

2. Markera “Conditional”.

3. Kopiera in följande kod till “Condition”. Detta för att vi inte behöver köra en del kod om det inte finns några nya fel:

//Only run this if there are any new errors in the error log to collect
answer = checkNewErrors();

function checkNewErrors() {
    try {
        var gr = new GlideRecord('syslog');
        //The query is the same as in the run script. In this request, were are checking to se if there are any errors from yesterday. Depending on when you run the scheduled job you may need to change the request query. Related error messages start with '[ERROR]' in this case. 
        gr.addEncodedQuery("sys_created_onONYesterday@javascript:gs.beginningOfYesterday()@javascript:gs.endOfYesterday()^level=2^messageSTARTSWITH[ERROR]");
        gr.query();
        return gr.hasNext();
    } catch (err) {
        var logUtil = new scLoggingUtil();
        logUtil.logError(err, "ERROR", "Scheduled job", "Create incident on error", "Condition", "n/a");

    }
}

4. Kopiera in följande kod till “Run this script”:

collectErrors();

//If there are any new errors, create an incident with information about the errors
function collectErrors() {
    try {

        //In this request, were are getting errors from yesterday. Depending on when you run the scheduled job you may need to change the request query. Related error messages start with '[ERROR]' in this case. 
        var logGa = new GlideAggregate('syslog');
        logGa.addEncodedQuery("sys_created_onONYesterday@javascript:gs.beginningOfYesterday()@javascript:gs.endOfYesterday()^level=2^messageSTARTSWITH[ERROR]");
        //We aggregate and group the messages so we don't get several rows for the same type of error.
        logGa.addAggregate('count');
        logGa.orderByAggregate('count');
        logGa.groupBy('message');
        logGa.query();
        var errors = "";
        //Loop through the result and create the text for the description field containing the number of occurences of the message and the message itself.
        while (logGa.next()) {
            var messageCount = logGa.getAggregate('count');
            errors += messageCount + "::" + logGa.message + "\n";
        }


        //Create incident

        //We want to assign the incident to the support group of ServiceNow. Hence, we need to get the CI which the support group is related to. You can use the sys_id of the support group directly but if you change supportgroup for the CI ServiceNow the incorrect support group will be assigned to the incident. 
        var grCI = new GlideRecord('cmdb_ci');
        var groupAssigned = grCI.get('869e6e47939f31003b4bb095e57ffbea');

        //Create the new incident. In this case we defined Short Description, Description, CI and Assignment Group. You may want to change/add data for the new incident. E.g. if default priority is not the one you want you may define the priority as well. 
        gr = new GlideRecord('incident');
        gr.initialize();
        gr.short_description = "Errors collected from the error log";
        gr.description = errors;
        gr.cmdb_ci = "869e6e47939f31003b4bb095e57ffbea";
        if (groupAssigned) {
            gr.assignment_group = grCI.support_group;
        }

        gr.insert();

    } catch (err) {
        var logUtil = new scLoggingUtil();
        logUtil.logError(err, "ERROR", "Scheduled job", "Create incident on error", "Run this script", "n/a");
    }
}

4. Du måste ändra de sys_id som är definierade i koden så de matchar den instans du jobbar med. I raden med grCI.get är det sys_id för det CI som gäller för ServiceNow (eller dom du vill använda något annat CI). Vi behöver en Support Group som är kopplad till det CI som används annars blir det problem när incidenten ska tilldelas en grupp.

Du kan så klart definiera vilka parametrar som ska sättas till vad enligt eget önskemål.

5. Spara

6. För att test behöver vi inte definiera något schema. Klicka på “Execute Now”.

7. Gå till incidenterna. Sortera så senaste incidenten kommer högst upp.

8. När du öppnar den ska den se ut liknande denna. Antal förekomster av listat fel står längs till vänster om själva felet:

incident.png

Sen är det bara att reda ut vad som blivit fel och fixa till det.

För att starta schemaläggningen definierar du schemat i Scheduled Script. Ovan lösning letar efter fel “Yesterday”. Rimligt är att köra jobbet mellan midnatt och start av arbetsdag så man har eventuella fel i en incident när man kommer till jobbet, kanske 01:00:

Schemaläggning av Scheduled Script

Schemaläggning av Scheduled Script