Tips_tutorials   >   Studiojs202   >   Studio JS 202 (All Contents)

Introduction

Welcome to the Studio JS 202 tutorial.

At the time of writing this tutorial I had not been monitoring my web apps to find out when and how often they were being used. When you create a web app it's nice to get some usage statistics, for yourself, and for your client.

In this tutorial we will delve into the area of monitoring and recording web app usage. It will be a learning experience for both of us!

You will also learn about sending emails from Omnis Studio and using timer objects.

This tutorial is provided to StudioTips Members only. If you are not currently a StudioTips Member please go to
www.studiotips.net and click the link provided for becoming a StudioTips Member. The cost of membership is minimal and helps to cover the time and expense of creating these tutorials and maintaining the studiotips.net website.

Class Wizard

If you use the Omnis Studio Class Wizard to create a remote task in one of the steps is asks if you want to inherit from a Monitor Task. If you select the checkbox it creates a web monitor task and window. Let's give this a try and then look at what the Omnis Studio Class Wizard creates for us.

  1. F2 Browser > Class Wizard > Remote Task
  2. Select the Plain Remote Task, enter rtWizard_Plain as the Remote Task Name, then click the Create button.
  3. The wizard will ask you if you want to inherit from a Monitor Task. Click the checkbox to select it. This will reveal a series of options.
  4. Select Create New Monitor Task.
    • Monitor Task Name: rtWizard_Monitor
    • Window Name: wMonitor
  5. Click through to the Finish.
The class wizard creates 3 new classes for you. rtWizard_Monitor, rtWizard_Plain, wMonitor. We can now study the code generated by the Omnis Studio wizard to figure out how it works.

Monitor Remote Task

The code in the rtWizard_Monitor is what we need to study to figure out how web app monitoring works.

There are 3 methods:

  1. $construct
    1. Checks to see if the wMonitor window is open and stores a reference to it.
    2. Sends it an $addConnection message to the wMonitor window
    3. The window returns a flag as to whether or not the server is busy.

  2. $destruct - Simply sends a $removeConnection message to the wMonitor window.

  3. $event - handles 3 different events.

    1. On evBusy - Sends a $setStatus message to the wMonitor window to indicate that the task is busy.
    2. On evIdle - Sends a $setStatus message to the wMonitor window to indicate that the task is not busy
    3. On evRejected - Calls the built-in $showmessage method to open an OK message on the client (web browser) displaying the pErrorText.

Monitor Window

The wMonitor window is keeping track of the following stats:

  1. Date/Time the server was last started.
  2. Date/Time the server was last paused.
  3. Number of times the server was started,
  4. Number of times the server was paused
  5. Total connections
  6. Total connections completed normally.
  7. Number of server transactions.
  8. Total disconnections.
  9. Total refused connections.
  10. Number of remote task instances.

The wMonitor window has a tasks list which displays each remote task that has been instantiated, the IP address which made the request, time connected, last response, time disconnected.

The wMonitor window has quite a few methods which you can study. We are not going to review the wMonitor window code in this tutorial. Feel free to study it on your own.

The problem I have with the web monitor created by the Omnis Studio Class Wizard is that it is a visual monitor. The code is inside a window class which must be instantiated for any monitoring to occur. If the window is closed, the monitoring ceases. The usage statistics are stored in memory, so when you close the window, the stats are lost.

Many web apps run remotely on a headless server, so it would make sense to create a non-visual object class which handles the web monitoring functions.

Non-Visual Web App Monitoring

Based on some of the code in rtWizard_Monitor and wMonitor we will build a non-visual web app monitoring object class.

Other features we would like to add to the web monitoring object class:

For the rest of the tutorial we will build a web monitoring object class with these features.

Data to Capture

Before we create the web monitoring object we need to decide what web app data we want to capture.

Here is the information which we are going to record in our database.

  1. Each new connection, the remote task class name, client's IP address.
  2. Each busy connection.
  3. Each rejected connection and the error message.

If the above information is saved to the database we can then report the following summary statistics from the database.

  1. Total connections. (Number of requests)
  2. Total busy connections.
  3. Total rejected connections.
  4. Number connections per client IP address.
  5. Number connections per remote task class.

In our database we will need a Webappstat table, with the following columns:

  1. ConnectionTime - Timestamp when the client request started
  2. ClientAddress - the IP address of the client
  3. RemoteTaskClassName - the name of the remote task which was instantiated
  4. ResponseSeconds - the number of seconds it took to process the request.
  5. EventCode - evRejected or evBusy if applicable.

Create Schema Class

  1. F2 Browser > select ContactsWeb library > New Class > Schema > name it sWebappstat
  2. Double-click sWebappstat to add the columns.
  3. Set the table name field to Webappstat
  4. Add the following columns to the schema class:
    1. ConnectionTime - Date Time - D m Y H:N:S
    2. ClientAddress - Character 15
    3. ResponseSeconds - Integer
    4. RemoteTaskClassName - Character 50
    5. EventCode - Character 15

  5. Set the No nulls property to kTrue for all of the columns.

Note

We are doing something a bit unorthodox by not including a primary key in this table. This is done to avoid the overhead caused by setting the primary key. We will only be inserting records into this table, no updates, so we can get away with skipping the primary key column.

Create Servertable

Create the Webappstat servertable by dragging the sWebappstat schema class onto the CONTACTS_01 session node of the SQL Browser root node in the F2 Browser treelist.

You can check your work by clicking the Tables child node of the CONTACTS_01 node and then double-clicking the Webappstat table. An Alter Table window opens showing the table columns.

Create Table Class

We should have a table class mapped to the sWebappstat schema class.

  1. Select the tCountry table class.
  2. Right-click Duplicate. Name the copy tWebappstat
  3. F6 Properties > set the $sqlclassname to sWebappstat
  4. Double-click tWebappstat.
  5. Change the $:DefaultOrderBy method to:

    Quit method "ORDER BY ConnectionTime DESC"

Create Web Monitor Object Class

We are ready to create the non-visual web monitor object class.

  1. F2 Browser > select ContactsWeb library > New Class > Object > name it oWebMonitor
  2. Double-click oWebMonitor to go to the class methods.
  3. Rename the $construct method to $initialize and enter the following code:

    Do iRow.$definefromsqlclass('tWebappstat')
    Do iRow.$sessionobject.$assign($ctask.dbsessionobj)
    Quit method kTrue



    Note

    The reason we are using a $initialize method rather than a $construct method in oWebMonitor object is the $intialize gives error checking control to the class which instantiates the object. You'll see this in more detail later in the tutorial.

  4. Add a method and name it $addConnection.
  5. Add a parameter, prTask, Item reference data type to the method.
  6. Add a parameter, pEventCode_opt, Long Integer data type to the method.
  7. Add the following code to the method.

    Calculate iRow.ConnectionTime as prTask.$connectiontime
    Calculate iRow.ClientAddress as prTask.$clientaddress
    Calculate iRow.ResponseSeconds as ddiff(kSecond,prTask.$connectiontime,#D)
    Calculate iRow.RemoteTaskClassName as prTask().$class().$name

    ; Set the event code to blank, unless it is evBusy or evRejected.
    If isnull(pEventCode_opt)
       Calculate iRow.EventCode as ''
    Else If pEventCode_opt=evBusy|pEventCode_opt=evRejected
       Calculate iRow.EventCode as pEventCode_opt
    Else
       Calculate iRow.EventCode as ''
    End If

    ; Insert the record in the database.
    Do iRow.$insert() Returns FlagOK
    If not(FlagOK)
       
       ; Log the SQL error.
       Calculate ErrText as iRow.$statementobject().$nativeerrortext
       Calculate Mssg as con('SQL Error on Insert',kCr,kCr,ErrText)
       Do errhndlr.$logError($cmethod,Mssg)
       
    End If

    Quit method FlagOK

Add webmon Startup_Task Variable

To avoid having to initialize the oWebMonitor object each time a request comes through the Omnis Web App Server we will initialize it once in the Startup_Task and then simply reference it when each remote task is initialized.

  1. Add the task variable, webmon, to the Startup_Task class.
  2. Set the data type to Object and point it to the oWebMonitor object class.
  3. Remove the following line of code which was added to the end of $construct method of the Startup_Task by the Class Wizard.

    Do $clib.$windows.wMonitor.$openonce() ;; added by Monitor remote task wizard
  4. Add the following code to the end of $construct method of the Startup_Task:

    ; Initialize the web monitoring object.
    Do webmon.$initialize() Returns FlagOK
    If not(FlagOK)
       Do errhndlr.$promptonceLastError()
    End If
    Quit method FlagOK



    The oWebMonitor class is initialized just once, when the library is opened. If there is an initialization error it is immediately reported.
Tip

The developer has little control over the $construct methods. Omnis Studio automatically calls the $construct method when the object is instantiated. If an error is logged by a $construct method, we really don't have a way of receiving the return flag from the $construct method, so the error would go by unnoticed. By using an $initialize method rather than a $construct method, the developer has control over when the initialization code is run. The developer receives the return flag and can properly notify the user of the error.

Create Remote Task Superclass

We will now create a remote task superclass which other remote tasks will be subclassed from. The remote task superclass will have all of the remote task - task variables and the web monitoring calls to the oWebMonitor object.

  1. F2 Browser > select ContactsWeb library > New Class > Remote Task > name it rtBase_abstract
  2. Double-click rtBase_abstract to go to the class methods.
  3. Add the instance variable, ioHTMLTools, Object type. Point it to oHTMLTools.
  4. Add the following task variables:
    1. dbsessionobj - Item reference
    2. errhndlr - Item reference
    3. webmon - Item reference

  5. Copy the setTaskVars method from rtCountryList to rtBase_abstract and modify the code so that it reads as follows:

    Do $itasks.[$clib().$name].$getTaskVarRef('errhndlr',errhndlr) Returns FlagOK
    If FlagOK
       Do $itasks.[$clib().$name].$getTaskVarRef('dbsessionobj',dbsessionobj) Returns FlagOK
       If FlagOK
          Do $itasks.[$clib().$name].$getTaskVarRef('webmon',webmon) Returns FlagOK
       End If
    End If
    Quit method FlagOK

  6. Add the following code to the $construct method.

    Do method setTaskVars Returns FlagOK
    Quit method FlagOK

  7. Add the following code to the $destruct method.

    Do webmon.$addConnection($cinst,pEventCode) Returns FlagOK
    Quit method FlagOK

  8. Add an $event method to rtBase_abstract.

  9. Add the following code to the $event method.

    On evBusy
    Do webmon.$addConnection($cinst,pEventCode)

    On evIdle

    On evRejected
    Do webmon.$addConnection($cinst,pEventCode)

Subclass rtCountryList

We now need to make rtCountryList a subclass of rtBase_abstract.

  1. F2 Brower > select rtCountryList > F6 Properties
  2. Set the $superclass property to rtBase_abstract

There are task variables and methods in the superclass which we need to inherit in the subclass.

  1. Double-click rtCountryList to get to the class methods.
  2. Right-click the instance variable ioHTMLTools > select Inherit Variable... > click Yes
  3. Right-click the task variable dbsessionobj > select Inherit Variable... > click Yes
  4. Repeat the above step for errhndlr.
  5. Setting the task variables is being handled by the superclass, so we can delete the setTaskVars method in rtCountry. Right-click the setTaskVars method and select Delete Selected Methods...
  6. Modify the start of the $construct method as follows so that it no longer calls the setTaskVars method, and instead calls the superclass $construct method.

    Do inherited Returns FlagOK
    If FlagOK

We are ready for testing our non-visual web monitoring object.

Web Monitoring Sequence

Here is the sequence of what happens with our web monitoring classes and methods.

  1. The ContactsWeb library is opened causing the $construct method of the Startup_Task class to execute.
  2. Near the end of the $construct method the oWebMonitor object class is instantiated by the task variable webmon and sent an $initialize message.

    The web app is now ready to receive HTTP requests.
  3. An HTTP request specifying the rtCountyList remote task class is received by the Omnis Web App Server.
  4. rtCountryList is instantiated by the Omnis Web App Server and a $construct message is sent to it.
  5. The $construct method of rtCountryList calls the superclass $construct method of rtBase_abstract which sets all of the remote task task variables by referencing them to the matching Startup_Task task variables.
  6. The $construct method of rtCountryList processes the request and returns the results as an HTML web page to the client via the Omnis Web App Server.
  7. A $destruct message is sent to the rtCountryList instance by the Omnis Web App Server. The superclass $destruct method is immediately called. The superclass $destruct method sends an $addConnection message to oWebMonitor.
  8. The $addConnection method prepares the row variable and inserts a record into the Webappstat table in the database.

I tried to get my ContactsWeb to generate an evBusy or evRejected event, but was unsuccessful in doing so. Should either or those events occur the following sequence would happen:

  1. If an evBusy or evRejected event occurs it would be caught by the $event method of rtBase_abstract
  2. An $addConnection message is sent to oWebMonitor.
  3. The $addConnection method prepares the row variable including with the pEventCode, and inserts a record into the Webappstat table in the database.

Web Monitoring Test

Time to test our web monitor classes and code.

  1. Close and reopen the ContactsWeb library in order to initialize the the oWebMonitor object class with the webmon task variable of the Startup_Task class.
  2. Put a blue breakpoint at the top of the $construct method of the rtCountry remote task.
  3. Put a blue breakpoint at the top of the $addConnection method of the oWebMonitor object class.
  4. Using your web browser open the searchcountries.htm web page which you created in the Studio JS 201 tutorial.

    http://localhost/searchcountries.htm
  5. Enter the first letter of a country (e.g. 'C') and click the Submit button on the web page.
  6. All going well you should hit the blue breakpoint in the $construct method of rtCountry.
  7. Click the Go button in the IDE toolbar.
  8. All going well you should hit the blue breakpoint in the $addConnection method of oWebMonitor.
  9. Step through the code and check to make sure the record is successfully inserted into the database.
  10. Click the Go button to finish
  11. Do a couple more searches from the searchcountries.htm web page.

We can now look at the database and view the Webappstat table records.

  1. F2 Browser > SQL Browser > CONTACT_01 session > Tables > Webappstat
  2. Click Show Data
  3. The Interactive SQL window will open displaying all of the Webappstat records.
There you have it! Any requests made to our Omnis Studio web app are being recorded in the database. We can now generate summary reports on the data to measure the useage of our web app.

Web App Stats Reports

We want to get summary statistics from the detailed Webappstat records. There are many ways to accomplish this. For this tutorial we'll add methods to the oWebMonitor object which return the stats in a list variable. The reason for doing this, is so that the results can be used for a report class, web page, or an email message.

What statistics would we like to know for a given time period? (Day, Week, Month, Year)

  1. Total number of connections.
  2. Number of connections from each unique client IP address.
  3. Nmber of connections for each remote task class.
  4. Max, min, and averge response time for the total connection, for each unique IP address, for each remote task class.
  5. Number of failed connections. (busy or rejected)

Create listdef Schema Class

I like to use dummy schema classes for defining lists which get passed around my application. This makes it easy to find the column names because they can be viewed in the F9 Catalog.

Create a list definition dummy schema class for our web app stats summary values.

  1. F2 Browser > ContactsWeb > New Class > Schema
  2. Name the class sWebStatsSummary_listdef
  3. Add the following columns to the listdef schema class.
    1. BeginDate - Short Date
    2. EndDate - Short Date
    3. TotalConnections - Integer
    4. FailedConnections - Integer
    5. MaxResponseSeconds - Integer
    6. MinResponseSeconds - Integer
    7. AvgResponseSeconds - Integer
    8. ClientAddress - Character 15
    9. RemoteTaskClassName - Character 50
  4. Close the schema class.

Web Stats List Method

There are numerous ways to compile the summary statistics. If you are an advanced SQL guru and using a 3rd party RDBMS you could get the statistics using SQL. Since we are working with the Omnis data file and I'm not an advanced SQL guru we'll compile the statistics using Omnis list notation.

The summary stats method is fairly lengthy. We won't go into detail explaining the code because learning SQL and learning lists notation is not the purpose of this tutorial.

  1. Add a new method, $retWebStatsList, to the oWebMonitor object.
  2. Add the following parameters to the method.
    1. pBeginDate - Short Date
    2. pEndDate - Short Date
    3. pkSubtotalPeriod - Long Integer - Default value zero (0)

  3. Add the following code to the method:

    ; Prepare the statement object using bind variables.
    Do dbsessionobj.$newstatement() Returns StmntObj
    Calculate SQLText as "SELECT * FROM Webappstat WHERE ConnectionTime >= @[BeginDate] AND ConnectionTime <= @[EndDate]"
    Do StmntObj.$prepare(SQLText) Returns FlagOK
    If not(FlagOK)
       Calculate Mssg as con("SQL Error occured while compiling Web App stats. ",kCr,"SQLText = ",SQLText,kCr,"Error = ",StmntObj.$nativeerrortext)
       Do errhndlr.$logError($cmethod,Mssg)
    Else
       
       ; Loop from the begin date to the end date in the specified subtotal period.
       If pBeginDate>pEndDate
          Calculate BeginDate as pEndDate
       Else
          Calculate BeginDate as pBeginDate
       End If
       Calculate DateTo as dadd(kDay,1,pEndDate)
       Calculate DateTo as dadd(kSecond,-1,DateTo)
       While BeginDate<=DateTo
          
          Switch pkSubtotalPeriod
             Case kDay
                Calculate EndDate as BeginDate
                Calculate NextBeginDate as dadd(kDay,1,BeginDate)
             Case kWeek
                Calculate EndDate as lday(kWeek,BeginDate)
                Calculate NextBeginDate as dadd(kDay,1,EndDate)
             Case kMonth
                Calculate EndDate as lday(kMonth,BeginDate)
                Calculate NextBeginDate as dadd(kDay,1,EndDate)
             Case kYear
                Calculate EndDate as lday(kYear,BeginDate)
                Calculate NextBeginDate as dadd(kDay,1,EndDate)
             Default
                Calculate EndDate as pEndDate
                Calculate NextBeginDate as dadd(kDay,1,EndDate)
          End Switch
          
          ; If the end date is greater than pDateTo, cut it back.
          If EndDate>pEndDate
             Calculate EndDate as pEndDate
          End If
          
          ; Move the end date to the last minute of the day.
          Calculate EndDate as dadd(kDay,1,EndDate)
          Calculate EndDate as dadd(kSecond,-1,EndDate)
          
          ; Execute the prepared SQL statement.
          Do StmntObj.$execute() Returns FlagOK
          If FlagOK
             
             ; Fetch the selected records.
             Do StmntObj.$fetch(FetchList,kFetchAll) Returns FetchStatus
             If FetchStatus=kFetchError
                Calculate FlagOK as kFalse
             End If
             
          End If
          If not(FlagOK)
             Calculate Mssg as con("SQL Error occured while compiling Web App stats.",kCr,StmntObj.$nativeerrortext)
             Do errhndlr.$logError($cmethod,Mssg)
             Break to end of loop
          Else
             
             ; Summarize the stats as per the specified subtotal period.
             Do method compileStatsList (BeginDate,EndDate,FetchList) Returns RetStatsList
             If StatsList.$linecount=0
                Calculate StatsList as RetStatsList
             Else
                Do StatsList.$merge(RetStatsList)
             End If
             
          End If
          
          ; Move the begin date to the 'next' begin date.
          Calculate BeginDate as NextBeginDate
          
       End While
       If not(FlagOK)
          Do StatsList.$define()
       End If
    End If
    Quit method StatsList

  4. Add a new method, complileStatsList, to the oWebMonitor object.

  5. Add the following parameters to the method.

    1. pBeginDate - Short Date
    2. pEndDate - Short Date
    3. pfFetchList - Field reference

  6. Add the following code to the method:

    ; Define the stats list.
    If iEmptyStatsList.$colcount=0
       Do iEmptyStatsList.$definefromsqlclass('sWebStatsSummary_listdef')
    End If
    Calculate StatsList as iEmptyStatsList

    Calculate MasterList as pfFetchList

    ; Set any event code to value of 1 (evBusy or evRejected)
    Do MasterList.$sendall($ref.EventCode.$assign(pick(len(MasterList.EventCode)>0,0,1)))

    Calculate TempList as MasterList

    ; Total connections
    Do StatsList.$add()
    Do StatsList.$line.$assign($ref.$linecount)
    Calculate StatsList.BeginDate as pBeginDate
    Calculate StatsList.EndDate as pEndDate
    Calculate StatsList.FailedConnections as TempList.$cols.EventCode.$total()
    Calculate StatsList.TotalConnections as TempList.$linecount-StatsList.FailedConnections
    Calculate StatsList.MaxResponseSeconds as TempList.$cols.ResponseSeconds.$maximum()
    Calculate StatsList.MinResponseSeconds as TempList.$cols.ResponseSeconds.$minimum()
    Calculate StatsList.AvgResponseSeconds as TempList.$cols.ResponseSeconds.$average()

    ; Connections by client address.
    Calculate TempList as MasterList
    While TempList.$linecount
       
       ; Select the clientaddress lines matching the first line.
       Do TempList.$search($ref.ClientAddress=TempList.1.ClientAddress)
       
       Do StatsList.$add()
       Do StatsList.$line.$assign($ref.$linecount)
       Calculate StatsList.BeginDate as pBeginDate
       Calculate StatsList.EndDate as pEndDate
       Calculate StatsList.FailedConnections as TempList.$cols.EventCode.$total(kTrue)
       Calculate StatsList.TotalConnections as TempList.$cols.ConnectionTime.$count(kTrue)-StatsList.FailedConnections
       Calculate StatsList.MaxResponseSeconds as TempList.$cols.ResponseSeconds.$maximum(kTrue)
       Calculate StatsList.MinResponseSeconds as TempList.$cols.ResponseSeconds.$minimum(kTrue)
       Calculate StatsList.AvgResponseSeconds as TempList.$cols.ResponseSeconds.$average(kTrue)
       Calculate StatsList.ClientAddress as TempList.1.ClientAddress
       
       ; Delete the selected clientaddress lines.
       Do TempList.$remove(kListDeleteSelected)
       
    End While

    ; Connections by remote task class.
    Calculate TempList as MasterList
    While TempList.$linecount
       
       ; Select the remote task class lines matching the first line.
       Do TempList.$search($ref.RemoteTaskClassName=TempList.1.RemoteTaskClassName)
       
       Do StatsList.$add()
       Do StatsList.$line.$assign($ref.$linecount)
       Calculate StatsList.BeginDate as pBeginDate
       Calculate StatsList.EndDate as pEndDate
       Calculate StatsList.FailedConnections as TempList.$cols.EventCode.$total(kTrue)
       Calculate StatsList.TotalConnections as TempList.$cols.ConnectionTime.$count(kTrue)-StatsList.FailedConnections
       Calculate StatsList.MaxResponseSeconds as TempList.$cols.ResponseSeconds.$maximum(kTrue)
       Calculate StatsList.MinResponseSeconds as TempList.$cols.ResponseSeconds.$minimum(kTrue)
       Calculate StatsList.AvgResponseSeconds as TempList.$cols.ResponseSeconds.$average(kTrue)
       Calculate StatsList.RemoteTaskClassName as TempList.1.RemoteTaskClassName
       
       ; Delete the selected remote task class lines.
       Do TempList.$remove(kListDeleteSelected)
       
    End While

    Quit method StatsList

Test Web Stats List Method

We can test the $retWebStatsList method from the Programmer Test Method of the Contacts menu.

  1. Contacts menu > Programmer Test Method...
  2. After the Breakpoint and Quit method enter the following code:

    Calculate BeginDate as fday(kMonth,#D)
    Calculate EndDate as lday(kMonth,#D)
    Do webmon.$retWebStatsList(BeginDate,EndDate) Returns #L1

  3. Contact menu > Programmer Test Method...
  4. Double-click the BeginDate line of code to set the Go point to that line.
  5. Click the Step-over button 3 times and then check the return value of #L1

Convert Web Stats List to Text

The web stats list could be put into a report, an HTML table, or converted to text.

We'll add a method to oWebMonitor to convert the web stats list to text.

  1. Add a new method, $convertWebStatsListToText, to the oWebMonitor object.
  2. Add the parameter, pfWebStatsList, Field Reference to the method.
  3. Add the following code to the method:

; Total connections.
Calculate List as pfWebStatsList

Do List.$search($ref.ClientAddress<>''|$ref.RemoteTaskClassName<>'')
Do List.$remove(kListDeleteSelected)
Do List.$sort($ref.BeginDate)

Calculate Text as con("Total Connections")
For List.$line from 1 to List.$linecount step 1
   
   Calculate Text as con(Text,kCr,List.BeginDate," - ",List.EndDate," : ",List.TotalConnections)
   If List.FailedConnections>0
      Calculate Text as con(Text," (",List.FailedConnections," Failed)")
   End If
   
End For

; Connections by Client IP Address
Calculate List as pfWebStatsList

Do List.$search($ref.ClientAddress<>'')
Do List.$remove(kListKeepSelected)
Do List.$sort($ref.BeginDate)

Calculate Text as con(Text,kCr,kCr,"Connections by Client IP Address")
While List.$linecount
   
   Do List.$search($ref.ClientAddress=List.1.ClientAddress)
   
   Calculate NumConn as List.$cols.TotalConnections.$total(kTrue)
   Calculate Text as con(Text,kCr,List.BeginDate," - ",List.EndDate," - ",List.1.ClientAddress," : ",NumConn)
   
   Calculate FailedNum as List.$cols.FailedConnections.$total(kTrue)
   If FailedNum>0
      Calculate Text as con(Text," (",FailedNum," Failed)")
   End If
   
   Do List.$remove(kListDeleteSelected)
   
End While

; Connections by Remote Task
Calculate List as pfWebStatsList

Do List.$search($ref.RemoteTaskClassName<>'')
Do List.$remove(kListKeepSelected)
Do List.$sort($ref.BeginDate)

Calculate Text as con(Text,kCr,kCr,"Connections by Remote Task Class")
While List.$linecount
   
   Do List.$search($ref.RemoteTaskClassName=List.1.RemoteTaskClassName)
   
   Calculate NumConn as List.$cols.TotalConnections.$total(kTrue)
   Calculate Text as con(Text,kCr,List.BeginDate," - ",List.EndDate," - ",List.1.RemoteTaskClassName," : ",NumConn)
   
   Calculate FailedNum as List.$cols.FailedConnections.$total(kTrue)
   If FailedNum>0
      Calculate Text as con(Text," (",FailedNum," Failed)")
   End If
   
   Do List.$remove(kListDeleteSelected)
   
End While

Quit method Text

Test Convert List To Text Method

We can test the $convertWebStatsListToText method from the Programmer Test Method of the Contacts menu.

  1. Contacts menu > Programmer Test Method...
  2. Add a line to the code so that the method reads as follows:

    Calculate BeginDate as fday(kMonth,#D)
    Calculate EndDate as lday(kMonth,#D)
    Do webmon.$retWebStatsList(BeginDate,EndDate) Returns #L1
    Do webmon.$convertWebStatsListToText(#L1) Returns #S1
    OK message {[#S1]}

  3. Contact menu > Programmer Test Method...
  4. Double-click the BeginDate line of code to set the Go point to that line.
  5. Click the Go button. All going well the OK message will report the web stats to you.

Web App Stats Web Page

In this section we will create a remote task and web page that allows a user to enter a date range and receive the current web app stats. In the real world you would put this web page in a password protected admin directory of the web site.

Create Remote Task

First we will create the remote task that responds to the request.

  1. F2 Browser > ContactsWeb library
  2. Right-click the rtBase_abstract class > select Make Subclass
  3. Name the subclass remote task, rtWebAppStats
  4. Override the $construct method.
  5. Add the parameter pParamsRow Row datatype to the method.
  6. Enter the following code in the method.

; Run the superclass code which set the task variables.
Do inherited Returns FlagOK
If FlagOK
   
   ; Convert the string text dates to dates vars.
   Calculate BeginDate as dat(pParamsRow.BeginDate,'y-M-D')
   Calculate EndDate as dat(pParamsRow.EndDate,'y-M-D')
   
   Do webmon.$retWebStatsList(BeginDate,EndDate) Returns List
   If List.$colcount=0
      Calculate FlagOK as kFalse
   Else
      
      Do webmon.$convertWebStatsListToText(List) Returns Text
      If len(Text)
         
         Calculate Text as replaceall(Text,kCr,'<br />')
         
         Do ioHTMLTools.$retHTMLPageTemplate Returns HTML
         
         ; Replace the placeholders with content.
         Calculate HTML as replaceall(HTML,'##LINK##','')
         Calculate HTML as replaceall(HTML,'##JAVASCRIPT##','')
         Calculate HTML as replaceall(HTML,'##TITLE##','Web App Stats')
         Calculate HTML as replaceall(HTML,'##BODY##',Text)
         
         ; Add the HTTP content header.
         Do ioHTMLTools.$addHTTPContentHeader(HTML) Returns FlagOK
         
      End If
   End If
End If
If not(FlagOK)
   
   ; An error occurred. Get the last error as an HTML page.
   Do ioHTMLTools.$retLastErrorHTML() Returns HTML
   
End If

Quit method HTML

Create Web Page

We will now create a web page for getting the web apps stats.

  1. Using your text editor create the following web page:

    <html>

    <head>
    <title>Web App Stats</title>
    <link rel="stylesheet" type="text/css" href="http://localhost/css/master.css" />
    <script type="text/javascript" src="../js/webappstats.js"></script>
    </head>

    <body>

    <div class="container"> <!-- open container div -->

    <div class="title">Web App Stats</div> <!-- title div -->

    <div class="search"> <!-- open search div -->

    <p>
    Enter the date range for the web stats which you would like to view
    and then click the 'View Stats' button
    </p>
    <table>
    <tr>
    <td>Begin Date</td>
    <td><input type="text" id="BeginDate" name="BeginDate" value="2007-01-01" size="15" /></td>
    <td>yyyy-mm-dd</td>
    </tr>
    <tr>
    <td>End Date</td>
    <td><input type="text" id="EndDate" name="EndDate" value="2007-12-31" size="15" /></td>
    <td>yyyy-mm-dd</td>
    </tr>
    <tr>
    <td></td>
    <td><input type="button" value="View Stats" onclick="submitRequestViaFrame()" /></td>
    </tr>
    </table>

    </div> <!-- close search div -->

    <iframe id="ResultsFrame" src="" width="100%" height="100%" scrolling="yes" frameborder="0"></iframe>

    </div> <!-- close container div -->

    <input type="hidden" id="WebAppServerCGI" value="http://localhost/cgi-bin/nph-omniscgi" />
    <input type="hidden" id="OmnisServer" value="5912" />
    <input type="hidden" id="OmnisLibrary" value="ContactsWeb" />
    <input type="hidden" id="OmnisClass" value="rtWebAppStats" />

    </body>
    </html>

  2. Save the web page as WebAppStats.htm to your web server folder.

Create JavaScript File

  1. Using your text editor create the following JavaScript file:

    function pingFile() {

    alert("Ping File");

    }

    function submitRequestViaFrame() {

    // Gather the hidden inputs needed to assemble the URL
    var CGI = document.getElementById("WebAppServerCGI").value
    var OmnisServer = document.getElementById("OmnisServer").value
    var OmnisLibrary = document.getElementById("OmnisLibrary").value
    var OmnisClass = document.getElementById("OmnisClass").value
    var BeginDate = document.getElementById("BeginDate").value
    var EndDate = document.getElementById("EndDate").value

    var URL = CGI + "?" + "OmnisServer=" + OmnisServer + "&OmnisLibrary=" + OmnisLibrary + "&OmnisClass=" + OmnisClass;
    //alert("URL = " + URL);
    URL = URL + "&BeginDate=" + BeginDate + "&EndDate=" + EndDate ;

    // Get a reference to the frame
    var rFrame = document.getElementById("ResultsFrame");

    // Point the frame to the URL
    rFrame.src = URL ;

    }

  2. Save the file as webappstats.js in the js directory of your web server.

Test Web Stats Web Page

  1. Open the web stats web page with FireFox

    http://localhost/WebAppStats.htm
  2. Click the View Stats button.

    All going well the web app stats will be returned to the lower frame of the web page.
  3. Click the View Stats button a few more times. The number of connections should increment each time you click the button.

There you have it! The ability to get your web app stats on-line.

We could get much fancier with the format and presentation of the web app stats report by putting them into a couple of tables on the web page and tinkering with the CSS. I'll leave that for you to do in your spare time. :-)

Web App Stats Email

I prefer to get an email automatically sent to me each week with the web app stats. In this section of the tutorial we'll add an email object and a timer object to accomplish this for us.

Email Object - Part 1

We could use the SMTPSend Omnis Command directly in our code, but that requires pulling together all of the parameters whenever we want to send an email. An easier way it to wrap the SMTPSend in an object class which we can initialize with the parameters that don't change and a few default parameters, then create a public $sendMail message with a reduced set of parameters. We can instantiate the oEmail object as a Startup_Task task variable, initialize it during startup, and then access it whenever we need to send an email from any method in our application.

  1. F2 Browser > ContactsWeb library > New Class > Object
  2. Name the class, oEmail
  3. Double-click oEmail to get to the methods.
  4. Rename $construct to $initialize
  5. Add the following character type parameters to the $initialize method.
    1. pSMTPServer
    2. pSMTPUser
    3. pSTMPPassword
    4. pPOP3Server
    5. pPOP3User
    6. pPOP3Password
    7. pDefaultFromEmailAddr
    8. pDefaultFromName

  6. Add the exact same set of instance variables, replacing the first letter p with the letter i

  7. Add the following code to the $initialize method.

Calculate iSMTPServer as pSMTPServer
Calculate iSMTPUser as pSMTPUser
Calculate iSMTPPassword as pSMTPPassword
Calculate iPOP3Server as pPOP3Server
Calculate iPOP3Password as pPop3Password
Calculate iPOP3User as pPOP3User
Calculate iDefaultFromEmail as pDefaultFromEmailAddr
Calculate iDefaultFromName as pDefaultFromName

Quit method kTrue

Email Object - Part 2

  1. Add a $sendMail method to oEmail.
  2. Add the following parameters to the $sendMail method.
    1. pSubject Character
    2. pBody Character
    3. pTo - Binary
    4. pCc_opt - Binary
    5. pBcc_opt - Binary
    6. pFromEmailAddr_opt - Character
    7. pFromName_opt - Character

  3. Add the following code to the $sendMail method.

; Ping the SMTP server before we attempt to send an email.
Do method pingSMTPServer Returns FlagOK
If FlagOK
   
   ; Set the optional 'from' parameters
   If len(pFromEmailAddr_opt)
      Calculate FromName as pFromEmailAddr_opt
   Else
      Calculate FromName as iDefaultFromEmail
   End If
   
   If len(pFromName_opt)
      Calculate FromName as pFromName_opt
   Else
      Calculate FromName as iDefaultFromName
   End If
   
   ; Convert any To, Cc, Bcc string parameters to lists.
   If pTo.$colcount
      Calculate ToList as pTo
   Else
      Do method convertCSVToList (pTo) Returns ToList
   End If
   
   If pCc_opt.$colcount
      Calculate CcList as pCc_opt
   Else
      Do method convertCSVToList (pCc_opt) Returns CcList
   End If
   
   If pBcc_opt.$colcount
      Calculate BccList as pBcc_opt
   Else
      Do method convertCSVToList (pBcc_opt) Returns BccList
   End If
   
   ; Add some extra header information to reduce the chance that this is classified as spam.
   Do XtraHdrsList.$define(HeaderType,Value)
   Do XtraHdrsList.$add('Content-Type','text/plain; charset=US-ASCII; format=flowed')
   Do XtraHdrsList.$add('Content-Transfer-Encoding','7bit')
   Do XtraHdrsList.$add('Mime-Version','1.0')
   
   ; Send the email
   ; SMTPSend (iSMTPServer,FromEmailAddr,ToList,pSubject,pBody,CcList,...
   ; ...BccList,FromName,,,XtraHdrsList,iSMTPUser,iSMTPPassword) Returns ErrCode
   SMTPSend (iSMTPServer,FromEmailAddr,ToList,pSubject,pBody,CcList,BccList,FromName,,,XtraHdrsList,iSMTPUser,iSMTPPassword) Returns ErrCode
   If ErrCode<>0
      Calculate Mssg as con("SMTPSend Error - Status error code = ",ErrCode)
      Do errhndlr.$logError($cmethod,Mssg)
      Calculate FlagOK as kFalse
   End If
   
End If
Quit method FlagOK

Email Object - Part 3

  1. Add a convertCSVToList method to oEmail.
  2. Add a parameter, pfTextString, Field reference to the method.
  3. Add the following code to the convertCSVToList method.

Calculate TextString as pfTextString
Do List.$cols.$add('value',kCharacter,kSimplechar,1000000)

While len(TextString)
   
   Calculate Value as trim(strtok('TextString',','))
   If len(Value)
      Do List.$add(Value)
   End If
   
End While

Quit method List

Initialize Email Object

We will instantiate and initialize the oEmail object from the Startup_Task class.

  1. Add the task variable, eml, Object type to the Startup_Task class.
  2. Point the eml task variable to the oEmail object class.
  3. Modify the end of the $construct method of the Startup_Task so that it reads as follows:

    ; Initialize the web monitoring object.
    Do webmon.$initialize() Returns FlagOK
    If FlagOK
       
       ; Initialize the email object.
       ; Do eml.$initialize(SMTPServer,SMTPUser,SMTPPassword,POP3Server,POP3User,...
       ; ...POP3Password,DefaultFromEmailAddr,DefaultFromName) Returns FlagOK
       Do eml.$initialize(SMTPServer,SMTPUser,SMTPPassword,POP3Server,POP3User,POP3Password,DefaultFromEmailAddr,DefaultFromName) Returns FlagOK
       
    End If
    Quit method FlagOK



    You will need to add the local variables that are sent as parameters with the $initialize message sent to the oEmail object.
  4. Set the value of each local variable in the Init Val/Calc column of the variables pane.
    e.g. SMTPServer - 'mail.vencor.ca', SMTPUser = 'doug@vencor.ca'

Test Email Object

You can test the oEmail object by sending an email to yourself using the Programmer Test Method.

  1. Close and reopen the ContactsWeb library in order to initialize the oEmail object.
  2. Contacts menu > Programmer Test Method...
  3. After the Breakpoint and Quit method enter the following code inserting your name and email address in the code.

    Calculate Subject as 'Test Message from oEmail'
    Calculate Body as 'This is a test'
    Calculate To as 'Your Name <your_email_address>'
    Do eml.$sendMail(Subject,Body,To) Returns FlagOK

  4. Contact menu > Programmer Test Method...
  5. Double-click the Calculate To line of code to set the Go point to that line.
  6. Click the Step-in button 4 times to step into the $sendMail method.
  7. Continue stepping through the code all the way through to sending the email. (SMTPSend)
  8. All going well the email will be sent to you through the SMTP server which you specified.
  9. Go to your email application and check your mail. Depending on the speed of the email server you should receive the email sent by the oEmail object within a few minutes.

Timer Object Emails

Now that we have the oEmail object working we can set up a timer object to email us web stats at the end of every day, week, month, year.

We need to make sure the Timer library external component is being loaded on startup.

  1. F2 Browser > External Components - opens the External Components window.
  2. Expand the External Components node in the treelist.
  3. Scroll down and select the Timer Library node.
  4. If there is a red circle icon next to the Timer Library node, click the Starting Omnis radio button, then click the OK button.
  5. Quit and reopen Omnis Studio if the Timer Library was not already loaded.

Create an object class that is subclassed from the timer external component.

  1. F2 Browser > New Class > Object
  2. Name the new class oWebStatsEmailTimer.
  3. F6 Properties > click the $superclass property combo button and scroll down to the External Components node.
  4. Expand the node and scroll down and select the Timer node, then click the OK button.

Timer Object Method $initialize

The timer object superclass has several public methods which are inherited by our oWebStatsEmailTimer object.

  1. Right-click oWebStatsEmailTimer and select the Interface Manager menu item. This opens the Interface Manager window.
  2. Click the Properties tab. You will see several properties in blue text. Scroll through them and read the Description at the bottom of the window. We will set some of these properties when we initialize the timer object.
  3. Click the Method tab in the Interface Manager window and double-click the $construct method. This takes you directly to the method in the IDE.
  4. Rename the $construct method to $initialize
  5. Add the following parameters to the method.
    1. pToEmailAddr - Character
    2. pkInterval - Long integer

  6. Add the following instance variables.

    1. ikInterval - Long integer
    2. iToEmailAddr - Character
    3. iEndDay - Short Date
    4. iEndWeek - Short Date
    5. iEndMonth - Short Date
    6. iEndYear - Short Date

  7. Enter the following code in the $initialize method.

    ; Set the properties.
    Calculate $cinst.$autoreset as kTrue
    Calculate $cinst.$reentrant as kFalse
    Calculate $cinst.$useseconds as kTrue
    Calculate $cinst.$timervalue as 60*60 ;; Hourly

    ; Copy the parameter values to ivars.
    Calculate iToEmailAddr as pToEmailAddr
    Calculate ikInterval as pkInterval

    ; Set the begin and the end period for the current interval period.
    Calculate iEndDay as #D
    Calculate iEndWeek as lday(kWeek,#D)
    Calculate iEndMonth as lday(kMonth,#D)
    Calculate iEndYear as lday(kYear,#D)

    Quit method kTrue

Timer Object Method $timer

The $timer method is called each time the timer reaches the specified $timervalue duration.

  1. Right-click the $timer method and select Override Method.
  2. Enter the following code in the $timer method.

    ; Is there another method currently running?
    Calculate StackList as sys(192)
    Calculate FlagOK as kTrue ;; Default the flag to true.

    If StackList.$linecount>1
       ; Do nothing, another method is running.
    Else
       
       ; Have we started the day after the current end date?
       If #D>iEndDay
          
          If ikInterval=kDay ;; kWeek, kMonth, kYear
             
             ; Time to send a web stats email.
             Calculate Interval as 'Daily'
             Do $cinst.$sendWebStatsEmail(iEndDay,iEndDay,Interval) Returns FlagOK
             
          End If
          If FlagOK
             
             ; Set the end day to today
             Calculate iEndDay as #D
             
          End If
          If FlagOK
             
             ; Have we started the day after the current end week?
             If #D>iEndWeek
                
                ; Check to make sure the interval is not monthly or yearly.
                If ikInterval<>kMonth&ikInterval<>kYear
                   
                   ; Time to send a web stats email.
                   Calculate BeginIntervalDate as fday(kWeek,iEndWeek)
                   Calculate Interval as 'Weekly'
                   Do $cinst.$sendWebStatsEmail(BeginIntervalDate,iEndWeek,Interval) Returns FlagOK
                   
                End If
                If FlagOK
                   Calculate iEndWeek as dadd(kWeek,1,iEndWeek)
                End If
                
             End If
          End If
          If FlagOK
             
             ; Have we started the day after the current end week?
             If #D>iEndMonth
                
                ; Check to make sure the interval is not yearly.
                If ikInterval<>kYear
                   
                   ; Time to send a web stats email.
                   Calculate BeginIntervalDate as fday(kMonth,iEndMonth)
                   Calculate Interval as 'Monthly'
                   Do $cinst.$sendWebStatsEmail(BeginIntervalDate,iEndMonth,Interval) Returns FlagOK
                   
                End If
                If FlagOK
                   Calculate iEndMonth as lday(kMonth,iEndDay)
                End If
                
             End If
          End If
          If FlagOK
             
             ; Have we started the day after the current end year?
             If #D>iEndYear
                
                ; Time to send a web stats email.
                Calculate BeginIntervalDate as fday(kYear,iEndYear)
                Calculate Interval as 'Yearly'
                Do $cinst.$sendWebStatsEmail(BeginIntervalDate,iEndYear,Interval) Returns FlagOK
                If FlagOK
                   Calculate iEndYear as lday(kMonth,iEndDay)
                End If
                
             End If
          End If
          
       Else
          
          ; Do nothing, autoreset will call the method again later.
          Calculate FlagOK as kTrue
          
       End If
    End If
    If not(FlagOK)
       
       ; We have to report an error to the user.
       ; Since this is a web server, we can't prompt the user.
       ; Send the error to the trace log.
       Do errhndlr.$getonceLastError(Mssg,Method)
       Send to trace log {----- START ERROR MESSAGE ---- [#D] ---- [$cmethod().$name] ----- [$ctask().$name] -----}
       Send to trace log {----- The '[Method]' method logged the following message:}
       Send to trace log {----- [Mssg]}
       Send to trace log {----- END ERROR MESSAGE ----}
       
    End If

    Quit method FlagOK

Timer Object Method $sendWebStatsEmail

  1. Add a $sendWebStatsEmail method to the oWebStatsEmailTimer class.
  2. Add the following parameters to the method.
    1. pBeginDate - Short Date
    2. pEndDate - Short Date
    3. pInterval - Character

  3. Enter the following code in the $sendWebStatsEmail method.

; Get the web stats for the current period.
Do webmon.$retWebStatsList(pBeginDate,pEndDate) Returns StatsList
If StatsList.$colcount
   
   ; Convert the list to text for the email body.
   Do webmon.$convertWebStatsListToText(StatsList) Returns Body
   If len(Body)
      
      ; Send the email.
      Calculate Subject as con("Web App Server Stats - ",pBeginDate," to ",pEndDate)
      If len(pInterval)>0
         Calculate Subject as con(pInterval," ",Subject)
      End If
      Calculate Body as con(kCr,Subject,kCr,kCr,Body)
      Do eml.$sendEmail(Subject,Body,iToEmailAddr) Returns FlagOK
   End If
   
End If
Quit method FlagOK

Initialize Timer Object

We will instantiate and initialize the oWebStatsEmailTimer object from the oWebMonitor object class.

  1. Add the instance variable, ioWebStatsEmailTimer, Object type to the oWebMonitor object class.
  2. Point the ioWebStatsEmailTimer task variable to the oWebStatsEmailTimer object class.
  3. Add the following parameters to the $initialize method of the oWebMonitor object class.
    1. pToEmailAddrWebStats - Character
    2. pkBaseIntervalEmailWebStats - Long Integer

  4. Modify the $initialize method of the oWebMonitor object class so that it reads as follows:

    Do iRow.$definefromsqlclass('tWebappstat')
    Do iRow.$sessionobject.$assign($ctask.dbsessionobj)

    ; Default flag to true.
    Calculate FlagOK as kTrue

    ; Initialize the oEmailWebStatsTime object if a 'To' email address has been provided.
    If len(pToEmailAddrWebStats)
       
       Do ioWebStatsEmailTimer.$initialize(pToEmailAddrWebStats,pkBaseIntervalEmailWebStats) Returns FlagOK
       If FlagOK
          
          ; Start the timer.
          Do ioWebStatsEmailTimer.$starttimer()
          
       End If
    End If

    Quit method FlagOK

  5. Add a $sendWebStatsEmail method to the oWebMonitor object class to facilitate testing the oWebStatsEmailTimer object.

  6. Add the following parameters to the method.

    1. pBeginDate - Short Date
    2. pEndDate - Short Date

  7. Add the following code to the method.

    Do ioWebStatsEmailTimer.$sendWebStatsEmail(pBeginDate,pEndDate) Returns FlagOK
    Quit method FlagOK

We need to modify the $construct method of the Startup_Task to send the parameters we added to the $initialize method of oWebMonitor.

  1. Go to the end of the $construct method of the Startup_Task
  2. Modify the code at Do webmon.$initialize(... to read as follows:

    ; Initialize the web monitoring object.
    Calculate ToEmailAddr as 'Your_Name <your_email_address>'
    Do webmon.$initialize(ToEmailAddr,kDay) Returns FlagOK
    If FlagOK
       
       ; Initialize the email object.
       Do eml.$initialize(SMTPServer,SMTPUser,SMTPPassword,POP3Server,POP3User,POP3Password,DefaultFromEmailAddr,DefaultFromName)
       
    End If
    Quit method FlagOK

Test Timer Object

Test the oWebStatsEmailTimer object using the Programmer Test Method.

  1. Close and reopen the ContactsWeb library in order to initialize the oWebStatsEmailTimer object.
  2. Contacts menu > Programmer Test Method...
  3. After the Breakpoint and Quit method enter the following code inserting your name and email address in the code.

    Calculate BeginDate as fday(kMonth,#D)
    Calculate EndDate as lday(kMonth,#D)
    Do webmon.$sendWebStatsEmail(BeginDate,EndDate) Returns FlagOK

  4. Contact menu > Programmer Test Method...
  5. Double-click the Do webmon.$sendWebStatsEmail line of code to set the Go point to that line.
  6. Click the Step-in button to step into the $sendWebStatsEmail method of the oWebMonitor object.
  7. Step in to the $sendWebStatsEmail method of the oWebStatsEmailTimer object.
  8. Step through the method by clicking the Step-over button repeatedly. All going well a web stats email will be sent to you.
  9. Go to your email application and check your mail. Depending on the speed of the email server you should receive the web stats email within a few minutes.
If you leave the ContactsWeb app open the oWebStatsEmailTimer object should automatically send you an email within an hour of the end of the specified interval period.

Summary

Well that wraps up the Studio JS 202 tutorial. I hope this tutorial was helpful to extending your understanding of web app monitoring, sending emails from Omnis Studio, and using timer objects.

If this tutorial has been helpful please send an emai to doug@vencor.ca. It's always encouraging to hear from developers who have benefited from these tutorials. Be sure to include any suggestions for improvements or additional topics you would like to see covered.

Visit www.studiotips.net to find out more and to become a StudioTips Member. Your support is greatly appreciated!

Happy coding!

Doug Kuyvenhoven
Vencor Software