Trackers

The source tracking APIs (/trackers) defined by TAS are for a candidate source tracking system based on fractional attribution.

With fractional attribution, an end result (such as someone applying for a job) can be mapped back to a whole chain of user interactions such as social sharing, emailing links, etc. This provides more detailed sourcing information than just tracking the last click that led the candidate to apply for the job.

There are two types of trackers:

Session trackers

Session trackers represent a unique candidate visit to a web site. Any candidate-facing web site (like a careers site, microsite, etc.) that is tracker aware should create session trackers when candidates use the site.

Campaign trackers

Campaign trackers represent a known starting point, such as an automated email alert or a job board posting.

Trackers are organised into trees. The path from any single leaf to the trunk of the tree represents the chain of sourcing events that led to a candidate outcome (like applying for a job).

Session trackers

Any candidate-facing web site (like a careers site, microsite, etc.) that is tracker aware should maintain session trackers when candidates use the site. This means:

  • Creating a session tracker for each new unique visit by a candidate
  • Connecting that tracker back to any tracker passed in as a url parameter

For example, when a candidate first follows a link like this to a page in a job board/careers site:

https://acme.supersite.com/marketing.html?tracker=1234

Then, the job board should create a session tracker and attach it to the tracker 1234. The resulting tracker tree will be like this:

1234
1287 (session tracker)

After this, the job board should attach the session tracker to all links that it emits, even for internal navigation around the site itself. For example, the candidate above might search for jobs at the site, and then click through to a specific job using a link like this:

https://acme.supersite.com/job=23&tracker=1287

Now the candidate might share the job via email using the browser's native share button. They email the job to a friend. The friend clicks through to the job. Because the friend represents a new unique visit, a new session tracker is created by the job board, linking back to tracker 1287. Now the tracker tree looks like this:

1234
1287 (session tracker)
1344 (session tracker)

If the friend now goes on to apply for the job, then the tracker will be passed into the applicant tracking system (ATS), giving it a deep insight into how the candidate came to hear about this job.

Sample logic for maintaining session trackers

The following example (in Java) shows one approach to maintaining session trackers.

This algorithm:

  • handles the case where the app does not add the tracker parameter to its own internal links (though ideally apps should).
  • optimises to avoid some unnecessary (not not harmful) calls to POST /trackers, but only remembers the most recent incoming tracker. This means, for example, that if the candidate has bookmarked a couple of links to the site, each with different values for the tracker url parameter, and then they pound those links repeatedly and alternately, then the app will repeatedly call POST /trackers (though this won't cause an explosion of trackers as existing ones will just be re-used).
Example

-- CODE language-java --
// get details of tracker (if any) passed in as a url parameter
Integer trackerParam = httpRequest.getParms("tracker");

// get details of existing session tracker (if any) and its parent (if any)
HttpSession session = httpRequest.getSession(true);
Integer sessionTracker = session.getAttribute("sessionTracker");
Integer parentTracker = session.getAttribute("parentTracker");

/*
We test if session tracker missing or mismatched with an incoming
tracker param - if so, potentially need to create a new one.
There are two ways to do this, based on whether the app emits
the tracker parameter on all its internal links (which might still
end up getting shared - so it should) or not.
*/

// Conditional when the app does emit tracker on all links
if (sessionTracker == null || ! Objects.equals(parentTracker, trackerParam)) {
// conditional when the app does not emit tracker on all links
// if (
//   sessionTracker == null ||
//   (trackerParam != null &&
//   (parentTracker == null || parentTracker != trackerParam)
// ) {

// prepare a request body for use with either create tracker API call
 String request =
 {
   "kind": "sessionCreation",
   "ip": httpRequest.getIP(),
   "Accept-Language": httpRequest..,
   "userAgent": httpRequest..
 }
 if (trackerParam == null) {
   // create a session tracker with no parent
   POST /trackers/root
     Request: <- request
     Location: -> trackerID
   session.setAttribute("sessionTracker", trackerID);
   session.removeAttribute("parentTracker");
 } else {
   // create a session tracker chained to the incoming tracker
   POST /trackers/byID/9022/trackers
     Request: <- request
     Location: -> trackerID
   session.setAttribute("sessionTracker", trackerID);
   session.setAttribute("parentTracker", parentTracker);
 }
}

/*
From here on, we will add (or replace) the sessionTracker
parm on any trackerized links we emit, e.g. "?tracker=9023"
One tiny problem is that the current URL (i.e. this entry page)
has the original (if any) tracker param on it.

That creates a problem in that if this page contained a non-TAS
social sharing plugin (one that just referenced the current page
rather than having tracker param passed into it), this new
session tracker would not be seen by it.

The solution is to try and force navigation to any new page as
quickly as possible - the new page's links will contain the
proper tracker param.
*/

Website analytics considerations for session trackers

Attaching the tracker parameter to all URLs that your site emits can affect your web site analytics. You may want to instruct analytics code to ignore the tracker parameter - for Google, see https://support.google.com/analytics/answer/1010249?hl=en.

Website search engine optimisation (SEO) considerations for session trackers

Attaching the tracker parameter to all URLs that your site emits can affect your web site's SEO. You may want to instruct search engines to ignore the tracker parameter - for Google, use Webmaster tools.

Scenarios

Scenario 1 - a legacy ATS uses tracker to pass through legacy source tracking information

Candidate arrives at the job details page on the legacy ATS, via a legacy link

The legacy ATS maintains candidateSource and device values in its session for incoming candidates.

A candidate arrives at:

acme/legacyats.com/jobs/Creative-Director-GR4022?candidateSource=facebook

The legacy ATS knows it will be handing off the apply process to some other microservices.

This means the apply process is out of the legacy ATS's control. But eventually the legacy ATS will regain control, when the apply app calls POST /candidates to push the candidate and job application in.

Therefore the legacy ATS needs to pack its legacy detail into a tracker, and pass that tracker in to the apply app, relying on the apply app eventually passing that tracker back in again.

ATS creates a session tracker

Like any good candidate-facing app, the legacy ATS maintains a session tracker, as per the logic above.

In this case the ATS also embeds the legacy sourcing data in the userArea field.

Not shown, but legacy ATS passes this tracker ID in as a URL parameter to the apply app in its job details page.

Request

-- CODE language-json --
{
  "sessionCreation": {
    "landingURI": "https://acme.jobboard.com/jobs/1044",
    "referer": "https://google.com",
    "userAgent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64)
    AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0
    Safari/537.36",
    "ipAddress": "192.168.1.1",
    "acceptLanguage": "en-us,en;q=0.5"
  },
  "userArea": {
    "legacyATS": {
      "candidateSource": "facebook",
      "device": "mobile"
    }
  }
}

Response

-- CODE language-json --
Location: 16044

Candidate clicks to apply at the SimpleApply microservice

ATS renders job details page. In it, a deep link like this leads to simpleapply:

acme.simpleapply.com/jobs/16670?tracker=16044

SimpleApply is tracker-aware and so creates a session tracker for the candidate (16045) and chains it to the existing tracker.

Once the candidate has completed the application, SimpleApply passes that tracker (16045) back into the ATS via the POST /candidates API call.

The ATS produces the API POST /candidates

The legacy ATS uses the following logic to extract its legacy source details from the incoming tracker:

Example

-- CODE language-js --
While (not found a tracker with a "legacyATS" field in the userArea) {
  if (found)
    use that for source details
  else
    The ATS attempts to manufacture its own source details
    from the immediately incoming tracker.
    
    It can only do a very limited job.
      1) the ATS searches up the tracker chain until it reaches any
      session tracker where device = derive from user-agent as per
      existing ATS behaviour
      2) The legacy ATS starts again and searches up the tracker
      chain until it reaches either:
        - a session tracker with a "sessionCreation.referer" value
          that matches one of the rows in its own table of media
        - a campaign tracker with a "campaign.referrer" value
          that matches one of the rows in its own table of media
        - a campaign tracker with a "campaign.referrer" value
          that matches one of the rows in its own table of media

      if (found)
        candidateSource = the media
}

Response

-- CODE language-json --
[
 {
   "meta": {
     "id": 16045,
     "ct": "acme",
     "ca": "jobboard",
     "created": "2015-11-05T13:15:30Z"
   },
   "sessionCreation": {
     "landingURI": "https://acme.jobboard.com/jobs/1044",
     "referer": "https://google.com",
     "userAgent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0
Safari/537.36",
     "ipAddress": "192.168.1.1",
     "acceptLanguage": "en-us,en;q=0.5"
   }
 },
 {
   "meta": {
     "id": 16045,
     "ct": "acme",
     "ca": "jobboard",
     "created": "2015-11-05T13:15:30Z"      
   },
   "campaign": {
     "campaignKind": "User defined",
     "objectType": "Job",
     "objectID": 16670
   },
   "userArea": {
     "candidateSource": "facebook",
     "deviceType": "mobile"
   }
 }
]

Scenario 2 - a job is advertised on a job board

A root campaign tracker is created:

Request

-- CODE language-json --
{
   "campaign": {
       "campaignKind": "Link out",
       "objectType": "Job",
       "objectID": 16670,
       "referrer": "http://www.seek.com.au"
   }
}

Response

-- CODE language-json --
Location: 9022

Scenario 3 - a job is emailed to a candidate who shares it with another candidate

An automated alert for a new job is emailed to a candidate

A root tracker is obtained by the mailout app:

Request

-- CODE language-json --
{
   "campaign": {
       "campaignKind": "Email alert",
       "objectType": "Job",
       "objectID": 16670,
       "accessCode": "kjy876gijhIUYGIY9786"
   }
}

Response

-- CODE language-json --
Location: 9022

The outgoing email has a link like this:

Example

-- CODE language-html --
<a href="/jobs/1004532?tracker=9022">Creative director</a>

That candidate visits the careers site app to view the job

The careers site is tracker-aware so creates a new session tracker.

Not interested themselves, candidates clicks to share the job details page on Facebook

The careers site may force navigation to a new page, so that any sharing plugins which rely on current URL have access to the up to date session tracker (see comments above).

One way is to hide the sharing plugins when the job details page is being generated for a new session, and instead show a button "Enable sharing" that just navigates to the current page - after navigation the page will not be being generated for a new session, so the sharing plugins will show as they should.

The candidate uses Facebook share button which shares the current URL

A friend sees their Facebook post and clicks through to view the job

The friend clicks through to the target page at /jobs/1004532?tracker=9023.

The same logic as above fires, and the careersite app creates a new session tracker (9024), and attaches it to all links it emits.

The friend is not interested in that job, but while browsing the careers site they find another job they are interested in

This job is at /jobs/26?tracker=9024.

Leaving for work, they do not have time to apply, so they share the link with themselves via email

They use sharing via email to send the link to themselves.

On their way to work on the bus, they open the email and return to the careers site on their mobile phone

they click through to the target page at /jobs/26?tracker=9024.

The same logic as above fires, and another new session tracker (9025) is created and used for all links.

They complete an application for the job

  • The tenant has set up SimpleApply to handle job applications
  • The candidate goes to acme.simpleapply.com/jobs/26?tracker=9025
  • SimpleApply is tracker aware as well, so another new session tracker is created (9026)
  • SimpleApply passes the application to the ATS, along with tracker=9026.

Scenario 4 - A job is shared via employee referral to a candidate who applies

The employee referral system (Refly) has a list of open jobs along with their apply links (via GET /jobs API)

Refly creates a new campaign tracker that will hold its own specific details, in this case that referral has been made by Fred.

Request

-- CODE language-json --
{
   "campaign": {
       "campaignKind": "Employee referral",
       "objectType": "Job",
       "objectID": 16670
   },
   "userArea": {
     "referredVia": "fred@acme.com"
   }
}

Response

-- CODE language-json --
Location: 9041

Refly now facilitates getting that link in front of candidate(s):

Example

-- CODE language-html --
<a href="/jobs/1004532?tracker=9041">Link text</a>

The candidate clicks on the link and arrives at the careers site

As previously, a new session tracker (9046) is created by the careers site and chained to the existing tracker.

Request

-- CODE language-json --
.. create session tracker

Response

-- CODE language-json --
Location: 9046

The candidate applies at SimpleApply

The tracker is passed into POST /candidates with the job application.

The ATS producing POST /candidates now notifies the agent insight app that some change has happened to an application (i.e. creation in this case):

Agent insight gets the application's tracker.

Response

-- CODE language-json --
Location: 9046

Agent insight now asks for every installed app that had some part in sourcing this application - i.e. that created a tracker upstream from 9046

Response

-- CODE language-json --
[
   {
       "meta": {
           "id": 10023,
           "ct": "acme",
           "ca": "jobboard",
   "created": "2015-11-05T13:15:30Z"            
       },
       "sessionCreation": {
           "landingURI": "https://acme.jobboard.com/jobs/1044",
           "referer": "https://google.com",
           "userAgent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0
Safari/537.36",
           "ipAddress": "192.168.1.1",
           "acceptLanguage": "en-us,en;q=0.5"
       }
   },
   {
       "meta": {
           "id": 10024,
           "ct": "acme",
           "ca": "refly",
      "created": "2015-11-05T13:15:30Z"            
       },
       "campaign": {
           "campaignKind": "User defined",
           "objectType": "Job",
           "objectID": 16670
       },
       "userArea": {
           "referredVia": "fred@acme.com"
       }
   }
]

Agent insight now uses the list of sourcing apps to alert each of the tracker owners that an application they have sourced has changed. Theoretically a sourcing app may appear multiple times in the chain.

Response

-- CODE language-json --
{
"operation": "insert",
"trackers": [ 9041 ]
}

Refly asks for specific information about the application. Agent insight internally re-creates the chain of trackers and verifies that Refly is allowed to ask for this application before responding

Response

-- CODE language-json --
{
"sourcerBucket": "interview",
"trackers": [ 9041 ]
}

Refly now has all the information it needs about the application that it had a part in sourcing.