Rutilus

Open-source customizable analytics

Log visitor activity

Log web page visits and more.

Add hooks to watch for custom actions, ex. scrolling down and clicking links or ads.

Analyze visit data

Query collected data to learn how well content performs and what kind of visitors are interested in it.

Profile visitors

Learn which topics visitors are interested in.

Recommend relevant content to keep visitors engaged.

Rutilus is easy to use.

Set up server modules

  • Node.js modules for each framework component
  • Modules can be run in same process or in different processes (and on different machines)
  • Dockerized bundle available in GitHub repo
// Import module constructors.
const Logger = require('rutilus-logger-node');
const Analytics = require('rutilus-analytics-node-js');

// Import your config object.
const config = require('./config');

// Instantiate and run the modules with your config object.
Logger(config, () => {
  console.log('Logger module is running!');
});
Analytics(config, () => {
  console.log('Analytics module is running!');
});

Add HTML hooks to web pages

  • Import Rutilus JavaScript file and add configuration to window object to point web pages at Logger module
  • Simplest hooks are CSS classes added to HTML elements
  • JavaScript function hooks allow you to observe and log any behavior DOM events can
<html>
<head>
  <meta charset="utf-8">
  <title>Your site</title>
  <script>
    window.rutilus = {
      config: {
        apiAddress: 'yourLoggerAddress'
      }
    }
  </script>
</head>
<body class="rutilus-body">
  <h1 class="rutilus-title3">Interesting Article Title</h1>

  <img src="somewhere" class="rutilus-body-image"/>

  <p>Interesting content.</p>

  <!-- If a user is authenticated, added by you to the DOM
  so that Rutilus can identify this user -->
  <input type="hidden" class="rutilus-user-id" value="123"/>
  <input type="hidden" class="rutilus-user-display-name" value="Bob"/>

  <video src="somewhere" class="rutilus-body-video"/>

  <button id="important-button" class="
    rutilus-listener
    rutilus-listen-for-click
    rutilus-listen-as-clickedOnImportantButton
  "/>

  <!-- The primary key of the article/product/etc, added by
  you to the DOM so that Rutilus can include it in the
  recommendation engine output -->
  <input type="hidden" class="rutilus-source-id" value="456"/>
  <input type="hidden" class="rutilus-source-type" value="article"/>
  <input type="hidden" class="rutilus-tags" value="tutorial,beginner"/>

  <!-- Using customization to log any data you want -->
  <input type="hidden" class="rutilus-favoriteColor" value="red"/>

  <div class="disqus-comments">
    <span class="comment-count">42 comments</span>
  </div>
  <footer class="rutilus-footer">
    <p>This is footer content.</p>
      <span class="rutilus-ad">Free cloud hosting from SkyNet.</span>
  </footer>
</body>
</html>

Logged data is stored in MongoDB

  • Each web page visit results in a Visit document.
  • Visit documents store:
    • Visitor location and platform/browser details
    • Username (if the visitor is registered and logged in)
    • All actions performed during the visit
    • Session information (visits tracked by browser cookies and all visits before cookies cleared are considered one session)
    • Resource information (article/product/etc data from web hooks)
  • $lookup operator not used - Rutilus is scalable with MongoDB sharding
  • Resource recommendations cached in database for high performance
[
  {
    // primary key of Visit document
    "_id": "58ebbda7ffef80649b622994",

    // actions the visitor performed during visit
    "actions": [
      {
        "date": 123456.0,
        "type": "CLICK_AD",
        "target": "http://some-ad-site.com/some-ad",
        "positionX": 1108.0,
        "positionY": 470,
        "sizeX": 100,
        "sizeY": 200,
        "isIframe": true,
        "classes": [
          "some",
          "css",
          "classes"
        ],
        "id": "google_ads_iframe_/...",
        "name": "google_ads_iframe_/...",
        "rawTag": "<iframe id="..."></iframe>"
      },
      {
        "date": 123456.0,
        "type": "CLICK_LINK",
        "target": "http://www.somesite.com/somepage",
        "classes": [
          "link",
          "external_link"
        ],
        "rawTag": "<a href="http://somesite.com/somepage" target="_blank" class="link external_link">some link</a>"
      },
      {
        "date": 123456.0,
        "type": "COPY_IMAGE",
        "imageUrl": "https://some-site.com/some-image.jpg"
      },
      {
        "date": 123456.0,
        "type": "COPY_TEXT",
        "text": "This is some text that the user found interesting enough to copy to their clipboard!"
      },
      {
        "date" : 123456.0,
        "type" : "FORM_START",
        "name" : "email",
        "rawTag" : "<input type="text" name="email" class="form-control" id="email" placeholder="Enter email here">",
        "id" : "subscribe-form",
        "attributes" : {
          "method" : "post",
          "action" : "/someActionDestination",
          "id" : "subscribe-form",
          "enctype" : "multipart/form-data"
        }
      },
      {
        "date": 123456.0,
        "type": "FORM_SUBMIT",
        "id" : "subscribe-form",
        "attributes" : {
          "method" : "post",
          "action" : "/someActionDestination",
          "id" : "subscribe-form",
          "enctype" : "multipart/form-data"
        },
        "inputContents": [
          {
            "inputName" : "email",
            "inputType" : "text",
            "content" : [
              "some_email_address@gmail.com"
            ]
          }
        ]
      },
      {
        "date": 123456.0,
        "type": "SCROLL",
        "flag": "STARTED_SCROLL"
      },
      {
        "date": 123456.0,
        "type": "SCROLL",
        "flag": "ARTICLE_MIDDLE"
      },
      {
        "date": 123456.0,
        "type": "SCROLL",
        "flag": "ARTICLE_BOTTOM"
      },
      {
        "date": 123456.0,
        "type": "SCROLL",
        "flag": "REACHED_FOOTER"
      },
      {
        "date": 123456.0,
        "type": "SCROLL",
        "flag": "PAGE_BOTTOM"
      },
      {
        "date": 123456.0,
        "type": "SELECT_TEXT",
        "text": "An interesting sentence fragment."
      },
      {
        "date": 123456.0,
        "type": "SOCIAL_MEDIA_FOLLOW",
        "text": "twitter"
      },
      {
        "date": 123456.0,
        "type": "SOCIAL_MEDIA_SHARE",
        "service": "facebook",
        "numCurrentShares": 20
      },
      {
        "date": 123456.0,
        "type": "TRIGGER",
        "triggerName": "clickedOnImportantButton"
      }
    ], // end of actions array
    // ads present on page during visit
    "ads": [
      {
        "target": "http://some-ad-site.com/some-ad",
        "positionX": 1108.0,
        "positionY": 470,
        "sizeX": 100,
        "sizeY": 200,
        "isIframe": true,
        "classes": [
          "some",
          "css",
          "classes"
        ],
        "id": "google_ads_iframe_/...",
        "name": "google_ads_iframe_/...",
        "rawTag": "<iframe id="..."></iframe>"
      }
    ],
    // name of resource's author
    "author": "Bob",
    // id of resource's author
    "authorId": "123",

    // details about the visitor's browser
    "browser": {
      "major": "12",
      "name": "Firefox",
      "version": "12.34"
    },

    // number of characters in body of resource
    "charsNo": 12,
    // visitor's city
    "city": "Townsville",
    // visitor's country
    "country": "United Hemispheres of Earth",
    // AddThis clickback value
    "clickback": "Facebook",
    // raw value of the site's
    // cookies in visitor's browser
    "cookies": "...",
    // whether or not visitor has cookies enabled
    "cookiesEnabled": true,
    // number of Disqus comments present on page
    // at beginning of visit
    "commentsNo": 5
    // details about visitor's CPU architecture
    "cpu": {
      "architecture": "amd64"
    },

    // date page view started (accurate to ms)
    "date": 123456.0,
    // date page view ended (accurate to ms)
    "dateClose": 234567.0,
    // details about visitor's device
    "device": {
      "model": "s8",
      "type": "mobile",
      "vendor": "samsung"
    },
    // width of visitor's browser viewport
    "documentSizeX": 1000,
    // height of visitor's browser viewport
    "documentSizeY": 2000,

    // details about visitor's browser engine
    "engine": {
      "name": "webkit",
      "version": 23.45
    },

    // from the customization in "hooks" above
    "favoriteColor": "red",

    // number of images present on page
    "imagesNo": 6,
    // visitor's IP address
    "ip": "123.456.789.012",

    // visitor's latitude
    "latitude": 12.3456,
    // number of milliseconds between beginning
    // of page view and DOMContentLoaded event
    "loadTime": 1000,
    // visitor's longitude
    "longitude": 23.4567,

    // details about visitor's operating system
    "operatingSystem": {
      "name": "Windows",
      "version": "10"
    },

    // number of paragraphs in resource's body
    "paragraphsNo": 3,

    // width of visitor's screen
    "screenSizeX": 1920,
    // height of visitor's screen
    "screenSizeY": 1080,
    // date of first visit in visit's session
    "sessionCreationDate": 123456.0,
    // resource's id
    "sourceId": "123",
    // resource's type
    "sourceType": "ARTICLE",

    // tags added to resource
    "tags": [
      "tutorial",
      "beginner"
    ],

    // resource's title
    "title": "Interesting Article Title",
    // URL of the resource
    "url": "https://yourSite.com/resourceSlug",
    // raw value of visitor's browser userAgent
    "userAgent": "...",


    // visitor's user id and userName (if they
    // are registered and logged in)
    "userId": "123",
    "userName": "Bob",
    // whether or not visitor using AdBlock
    "usingAdBlock": false,
    // whether or not visitor using mobile device
    "usingMobile": false,

    // number of videos present on page
    "videosNo": 2,

    // count of each word in resource's body
    "wordsInBody": {
      "a": 1,
      "b": 2,
      "c": 4
    },
    // count of each word in resource's title
    "wordsInTitle": {
      "x": 2,
      "y": 4,
      "z": 8
    },
    // number of words in resource's body
    "wordsNo": 200
  }
]

Rutilus is powerful.

Analyze logged data

  • Export logged data as JSON and convert to CSV
  • Understand your audience:
    • Which articles result in ads clicked?
    • Which products do visitors share on social media?
    • Are visitors more engaged than they were last quarter?
userId # of Visits # of Facebook Shares and much more...
123 1 1 ...
234 2 5 ...
345 6 0 ...

HTTP API provides recommendations

  • Logged data for all visitors is used to calculate recommendations for each visitor
  • Persona provides recommendations based on the visitor's shared personal information - "who they claim to be"
  • Profile provides recommendations based on the visitor's browsing habits - "who they browse similar to"
curl -XGET 'apiAddress:3000/affinity/userId=123'
{
  "affinity": {
    "section": [
      {
        "sourceType": "ARTICLES",
        "score": 0.8,
        "average": 0.4,
        "count": 2,
        "relevance": 100,
        "tagScores": [
          {
            "tag": "basketweaving",
            "score": 0.1,
            "average": 0.1,
            "count": 1,
            "relevance": 0.5
          },
          {
            "tag": "3D printing",
            "score": 0.7,
            "average": 0.7,
            "count": 1,
            "relevance": 0.5
          }
        ]
      }
    ],
    "author": [
      {
        "author": "David Bowie",
        "score": 0.8,
        "count": 2,
        "average": 0.4,
        "relevance": 100
      }
    ],
    "authorId": [
      {
        "authorId": 1,
        "score": 0.8,
        "count": 2,
        "average": 0.4,
        "relevance": 100
      }
    ]
  },
  "persona": {
    "jobTitle": [
      {
        "title": "graphic designer",
        "score": 0.456
      }
    ],
    // resources visited by visitors with similar
    // shared personal information
    "resourcesVisited": {
      "ARTICLES": [
        {
          "tag": "graphic design",
          "sourceId": "abc123",
          "score": 0.5,
          "tagRelevance": 1,
          "authorRelevance": 1,
          "authorIdRelevance": 1,
          "scoreRelevance": 0.5
        }
      ]
    }
  },
  "profile": {
    "jobTitle": [
      {
        "title": "robotics engineer",
        "score": 123.45
      },
      {
        "title": "programmer",
        "score": 134.56
      }
    ],
    // resources visited by visitors with similiar
    // browsing habits
    "resourcesVisited": {
      "ARTICLES": [
        {
          "tag": "space exploration",
          "sourceId": "def456",
          "score": 150,
          "tagRelevance": 12.34,
          "authorRelevance": 0.5,
          "authorIdRelevance": 0.5,
          "scoreRelevance": 0.4
        },
        {
          "tag": "web development",
          "sourceId": "ghi789",
          "score": 200,
          "tagRelevance": 23.45,
          "authorRelevance": 0.7,
          "authorIdRelevance": 0.7,
          "scoreRelevance": 0.6
        }
      ]
    }
  },
  "user": {
    "jobTitle": "graphic designer",
    // the visitor's actual resources visited
    "resourcesVisited": [
      {
        "sourceType": "ARTICLES",
        "ids": [
          {
            "tag": "cake baking",
            "sourceId": "12345",
            "hitId": "abc123",
            "author": "Martha Stewart",
            "authorId": 23456
          },
          {
            "tag": "drone flying",
            "sourceId": "23456",
            "hitId": "def456",
            "author": "Patrick Stewart",
            "authorId": 34567
          }
        ]
      }
    ]
  }
}