Skip to content

Commit

Permalink
Pixel-based analytics script
Browse files Browse the repository at this point in the history
  • Loading branch information
rla committed Jan 7, 2019
1 parent 66f4b22 commit 3183661
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 12 deletions.
5 changes: 5 additions & 0 deletions admin/lib/pages/analytics.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ <h2>Reader analytics</h2>
id="analytics-duration"
class="form-control bc-margin-top-sm">
</select>
<span class="help-block">
Pixel-based tracking will not track page view duration. Session duration
in that case will be calculated as a difference between the first and the last page visit.
Sessions with a single pageview will have duration 0.
</span>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion admin/lib/pages/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ exports.create = function() {
months: months,
startMonth: ko.observable(currentDate.toISOString().substring(0, 7)),
endMonth: ko.observable(startDate.toISOString().substring(0, 7)),
duration: ko.observable('30'),
duration: ko.observable('0'),
results: {
summary: {
user_count: ko.observable(0),
Expand Down
81 changes: 81 additions & 0 deletions analytics/readers.image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Pixel-based analytics.
// Generates user and session identifiers on client side.
// Shortcuts bots/crawlers/spiders.

(() => {
const platform = navigator.platform || '';
if (platform.match(/bot|crawler|spider/i)) {
// Skips GoogleBot etc.
return;
}

// Helper to check whether the current visitor is the
// blog administrator.
const isAdmin = () => {
return document.cookie.match(/blog_core_admin\s*=\s*1/);
};

// Don't run the remainder of the script for the admin.
if (isAdmin()) {
return;
}

// Helper to generate random identifiers.
const CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789';
const generateId = () => {
let id = '';
for (let i = 0; i < 32; i++) {
id += CHARACTERS.charAt(Math.floor(Math.random() * CHARACTERS.length));
}
return id;
};

// Reads the previous user id or generates a new one.
const getUserId = () => {
const match = document.cookie.match(/readers_user\s*=\s*([a-z0-9\-]+)/);
if (match) {
return match[1];
}
const id = generateId();
const expires = new Date();
expires.setFullYear(expires.getFullYear() + 1);
document.cookie = 'readers_user=' + id + '; path=/; expires=' + expires.toUTCString();
return id;
};

// Reads the previous user id or generates a new one.
const getSessionId = () => {
const match = document.cookie.match(/readers_session\s*=\s*([a-z0-9\-]+)/);
if (match) {
return match[1];
}
const id = generateId();
document.cookie = 'readers_session=' + id + '; path=/';
return id;
};

// Injects the tracking pixel into the page body.
const injectPixel = () => {
const image = document.createElement('img');
image.width = 1;
image.height = 1;
image.style.position = 'fixed';
image.style.bottom = '0';
image.style.right = '0';
image.alt = 'Reader information sent to the backend';
const params = {
u: getUserId(),
s: getSessionId(),
p: navigator.platform,
t: document.title,
e: window.bcEntryId || null,
r: document.referrer || null
};
const query = Object.keys(params).map(
k => `${k}=${encodeURIComponent(params[k])}`).join('&');
image.src = `/bc/reader.png?${query}`;
document.body.appendChild(image);
};

injectPixel();
})();
2 changes: 1 addition & 1 deletion pack.pl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name('blog_core').
version('1.4.0').
version('1.5.0').
title('Blog/CMS framework').
author('Raivo Laanemets', 'https://rlaanemets.com/').
home('http://blog-core.net/').
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "blog-core",
"version": "1.4.0",
"version": "1.5.0",
"private": true,
"devDependencies": {
"@babel/core": "^7.2.0",
Expand All @@ -12,6 +12,7 @@
},
"scripts": {
"build-readers": "webpack --mode production --config webpack.readers.js",
"build-readers-image": "webpack --mode production --config webpack.readers.image.js",
"build-admin": "webpack --mode production --config webpack.admin.js",
"build-admin-watch": "webpack --watch --mode production --config webpack.admin.js"
}
Expand Down
10 changes: 10 additions & 0 deletions prolog/bc/bc_analytics.pl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
:- module(bc_analytics, [
bc_analytics_record_pixel/1, % +Data
bc_analytics_record_user/2, % +Data, -UserId
bc_analytics_record_session/2, % +Data, -SessionId
bc_analytics_record_pageview/2, % +Data, -PageviewId
Expand All @@ -20,6 +21,15 @@
:- dynamic(stream/1).
:- dynamic(open_month/2).

% Stores pixel-based analytics data.

bc_analytics_record_pixel(Data):-
Data.user_id = null, !.

bc_analytics_record_pixel(Data):-
integer_timestamp(TimeStamp),
record_entry(Data.put(timestamp, TimeStamp)).

% Stores the user data while generating the
% new random user identifier.

Expand Down
12 changes: 11 additions & 1 deletion prolog/bc/bc_analytics_db.pl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
:- use_module(library(assoc)).
:- use_module(library(error)).
:- use_module(library(debug)).
:- use_module(bc_env).
:- use_module(bc_analytics).
:- use_module(bc_analytics_read).
:- use_module(bc_analytics_ts).
Expand Down Expand Up @@ -50,7 +51,16 @@
bc_analytics_flush_output,
bc_analytics_read(From, To, Module),
get_time(TimeStamp),
assertz(analytics_cache(Interval, Module, TimeStamp))).
assert_analytics_cache(Interval, Module, TimeStamp)).

% Stores the cache entry only in the production
% environment.

assert_analytics_cache(Interval, Module, TimeStamp):-
bc_env_production, !,
assertz(analytics_cache(Interval, Module, TimeStamp)).

assert_analytics_cache(_, _, _).

% Timeseries analytics.

Expand Down
135 changes: 129 additions & 6 deletions prolog/bc/bc_analytics_read.pl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:- use_module(library(error)).
:- use_module(library(gensym)).
:- use_module(library(pcre)).
:- use_module(library(gensym)).
:- use_module(bc_analytics).

% Reads analytics data into the given module.
Expand All @@ -18,8 +19,27 @@
must_be(ground, To),
gensym(analytics_cache_module_, Module),
dynamic(Module:user/1),
dynamic(Module:user_pixel/1),
dynamic(Module:user_duration/2),
dynamic(Module:user_timestamp/2),
dynamic(Module:user_session_count/2),
dynamic(Module:user_pagecount/2),
dynamic(Module:session/1),
dynamic(Module:session_pixel/1),
dynamic(Module:session_user/2),
dynamic(Module:session_duration/2),
dynamic(Module:session_pagecount/2),
dynamic(Module:session_agent/2),
dynamic(Module:session_platform/2),
dynamic(Module:pageview/1),
dynamic(Module:pageview_pixel/1),
dynamic(Module:pageview_session/2),
dynamic(Module:pageview_duration/2),
dynamic(Module:pageview_timestamp/2),
dynamic(Module:pageview_location/2),
dynamic(Module:pageview_referrer/2),
dynamic(Module:pageview_title/2),
dynamic(Module:pageview_entry/2),
findall(Name, file_name(From, To, Name), Names),
maplist(read_file_into(Module), Names),
compute_session_pagecounts(Module),
Expand Down Expand Up @@ -93,27 +113,121 @@
retractall(Module:pageview_duration(PageviewId, _)),
assertz(Module:pageview_duration(PageviewId, Dict.elapsed)).

load_dict_term_into(pixel, Module, Dict):-
_{
user_id: UserId,
agent: Agent,
platform: Platform,
session_id: SessionId,
location: Location,
referrer: Referrer,
entry_id: EntryId,
title: Title,
timestamp: TimeStamp
} :< Dict,
load_pixel_user(Module, UserId, SessionId, TimeStamp),
load_pixel_session(Module, UserId, SessionId, TimeStamp, Agent, Platform),
load_pixel_pageview(Module, SessionId, TimeStamp, Location, Referrer, Title, EntryId).

load_dict_term_into(_, _, _).

% Loads user data from a pixel tracking event.
% Updates user duration, page and session count.

load_pixel_user(Module, UserId, SessionId, TimeStamp):-
call(Module:user(UserId)), !,
call(Module:user_timestamp(UserId, OldTimeStamp)),
call(Module:user_pagecount(UserId, OldPageCount)),
call(Module:user_session_count(UserId, OldSessionCount)),
Duration is TimeStamp - OldTimeStamp,
PageCount is OldPageCount + 1,
( call(Module:session_user(SessionId, UserId))
-> SessionCount = OldSessionCount
; SessionCount is OldSessionCount + 1),
retractall(Module:user_duration(UserId, _)),
retractall(Module:user_pagecount(UserId, _)),
retractall(Module:user_session_count(UserId, _)),
assertz(Module:user_duration(UserId, Duration)),
assertz(Module:user_pagecount(UserId, PageCount)),
assertz(Module:user_session_count(UserId, SessionCount)).

% Loads user data from a pixel tracking event.
% Sets initial duration, page and session count.

load_pixel_user(Module, UserId, _, TimeStamp):-
assertz(Module:user(UserId)),
assertz(Module:user_pixel(UserId)),
assertz(Module:user_duration(UserId, 0)),
assertz(Module:user_timestamp(UserId, TimeStamp)),
assertz(Module:user_session_count(UserId, 1)),
assertz(Module:user_pagecount(UserId, 1)).

% Loads session data from a pixel tracking event.
% Updates session duration and page count.

load_pixel_session(Module, _, SessionId, TimeStamp, _, _):-
call(Module:session(SessionId)), !,
call(Module:session_pagecount(SessionId, OldPageCount)),
call(Module:session_timestamp(SessionId, OldTimeStamp)),
Duration is TimeStamp - OldTimeStamp,
PageCount is OldPageCount + 1,
retractall(Module:session_duration(SessionId, _)),
retractall(Module:session_pagecount(SessionId, _)),
assertz(Module:session_pagecount(SessionId, PageCount)),
assertz(Module:session_duration(SessionId, Duration)).

% Loads session data from a pixel tracking event.
% Sets initial session duration and page count, user agent and platform.

load_pixel_session(Module, UserId, SessionId, TimeStamp, Agent, Platform):-
assertz(Module:session(SessionId)),
assertz(Module:session_pixel(SessionId)),
assertz(Module:session_user(SessionId, UserId)),
assertz(Module:session_duration(SessionId, 0)),
assertz(Module:session_pagecount(SessionId, 0)),
assertz(Module:session_timestamp(SessionId, TimeStamp)),
assertz(Module:session_agent(SessionId, Agent)),
assertz(Module:session_platform(SessionId, Platform)).

% Loads pageview data from pixel tracing event.

load_pixel_pageview(Module, SessionId, TimeStamp, Location, Referrer, Title, EntryId):-
gensym(pv_, PageviewId),
assertz(Module:pageview(PageviewId)),
assertz(Module:pageview_session(PageviewId, SessionId)),
assertz(Module:pageview_duration(PageviewId, 0)),
assertz(Module:pageview_timestamp(PageviewId, TimeStamp)),
assertz(Module:pageview_location(PageviewId, Location)),
assertz(Module:pageview_referrer(PageviewId, Referrer)),
assertz(Module:pageview_title(PageviewId, Title)),
assertz(Module:pageview_entry(PageviewId, EntryId)).

% Computes total session durations from pageview
% durations.

compute_session_durations(Module):-
findall(SessionId, call(Module:session(SessionId)), Sessions),
findall(SessionId, (
call(Module:session(SessionId)),
\+ call(Module:session_pixel(SessionId))
), Sessions),
maplist(compute_session_duration(Module), Sessions).

compute_session_duration(Module, SessionId):-
findall(Duration, (
call(Module:pageview_session(PageviewId, SessionId)),
call(Module:pageview_duration(PageviewId, Duration))), Durations),
call(Module:pageview_duration(PageviewId, Duration))
), Durations),
sum_list(Durations, Total),
retractall(Module:session_duration(SessionId, _)),
assertz(Module:session_duration(SessionId, Total)).

% Computes total user durations from session durations.

compute_user_durations(Module):-
findall(UserId, call(Module:user(UserId)), Users),
findall(UserId, (
call(Module:user(UserId)),
\+ call(Module:user_pixel(UserId))
), Users),
maplist(compute_user_duration(Module), Users).

compute_user_duration(Module, UserId):-
Expand All @@ -128,7 +242,10 @@
% session page views.

compute_user_pagecounts(Module):-
findall(UserId, call(Module:user(UserId)), Users),
findall(UserId, (
call(Module:user(UserId)),
\+ call(Module:user_pixel(UserId))
), Users),
maplist(compute_user_pagecount(Module), Users).

compute_user_pagecount(Module, UserId):-
Expand All @@ -142,7 +259,10 @@
% Computes the number of sessions for the user.

compute_user_session_counts(Module):-
findall(UserId, call(Module:user(UserId)), Users),
findall(UserId, (
call(Module:user(UserId)),
\+ call(Module:user_pixel(UserId))
), Users),
maplist(compute_user_session_count(Module), Users).

compute_user_session_count(Module, UserId):-
Expand All @@ -154,7 +274,10 @@
% Computes the number of pagecounts for the sessions.

compute_session_pagecounts(Module):-
findall(SessionId, call(Module:session(SessionId)), Sessions),
findall(SessionId, (
call(Module:session(SessionId)),
\+ call(Module:session_pixel(SessionId))
), Sessions),
maplist(compute_session_pagecount(Module), Sessions).

compute_session_pagecount(Module, SessionId):-
Expand Down
Loading

0 comments on commit 3183661

Please sign in to comment.