1:- module(bc_analytics, [
    2    bc_analytics_record_pixel/1,            % +Data
    3    bc_analytics_record_user/2,             % +Data, -UserId
    4    bc_analytics_record_session/2,          % +Data, -SessionId
    5    bc_analytics_record_pageview/2,         % +Data, -PageviewId
    6    bc_analytics_record_pageview_extend/1,  % +Data
    7    bc_enable_analytics/1,                  % +Path
    8    bc_analytics_flush_output/0,
    9    bc_month_file_name/3                    % + Year, +Month, -File
   10]).

Generic visitor tracking analytics */

   14:- use_module(library(url)).   15:- use_module(library(debug)).   16:- use_module(library(error)).   17:- use_module(library(docstore)).   18
   19:- dynamic(enabled/0).   20:- dynamic(path/1).   21:- dynamic(stream/1).   22:- dynamic(open_month/2).   23
   24% Stores pixel-based analytics data.
   25
   26bc_analytics_record_pixel(Data):-
   27    Data.user_id = null, !.
   28
   29bc_analytics_record_pixel(Data):-
   30    integer_timestamp(TimeStamp),
   31    record_entry(Data.put(timestamp, TimeStamp)).
   32
   33% Stores the user data while generating the
   34% new random user identifier.
   35
   36bc_analytics_record_user(UserData, UserId):-
   37    must_be(dict, UserData),
   38    ds_uuid(UserId),
   39    integer_timestamp(TimeStamp),
   40    record_entry(UserData.put(_{
   41        user_id: UserId,
   42        timestamp: TimeStamp})).
   43
   44% Stores the session data while generating the
   45% new random session identifier.
   46
   47bc_analytics_record_session(SessionData, SessionId):-
   48    must_be(dict, SessionData),
   49    ds_uuid(SessionId),
   50    integer_timestamp(TimeStamp),
   51    record_entry(SessionData.put(_{
   52        session_id: SessionId,
   53        timestamp: TimeStamp})).
   54
   55bc_analytics_record_pageview(PageviewData, PageviewId):-
   56    must_be(dict, PageviewData),
   57    (   get_dict(pageview_id, PageviewData, PageviewId)
   58    ->  true
   59    ;   ds_uuid(PageviewId)),
   60    parse_url(PageviewData.location, UrlParts),
   61    memberchk(path(Path), UrlParts),
   62    integer_timestamp(TimeStamp),
   63    record_entry(PageviewData.put(_{
   64        location: Path,
   65        pageview_id: PageviewId,        
   66        timestamp: TimeStamp})).
   67
   68bc_analytics_record_pageview_extend(PageviewData):-
   69    record_entry(PageviewData).
   70
   71% Timestamp rounded as an integer. Leads
   72% to a more compact log files.
   73
   74integer_timestamp(TimeStamp):-
   75    get_time(Time),
   76    TimeStamp is round(Time).
   77
   78record_entry(Entry):-    
   79    with_mutex(serialized_write, record_entry_unsafe(Entry)).
   80
   81record_entry_unsafe(_):-
   82    \+ enabled, !.
   83
   84record_entry_unsafe(Entry):- 
   85    output_stream(Stream),
   86    write_canonical_term(Stream, Entry).
   87
   88output_stream(Stream):-
   89    (   open_month(Year, Month)
   90    ->  (   current_month(Year, Month)
   91        ->  stream(Stream)
   92        ;   open_output_stream(Stream))
   93    ;   open_output_stream(Stream)).
   94
   95% Opens a fresh output stream.
   96% This happens when a month boundary is crossed.
   97
   98open_output_stream(Stream):-
   99    (   stream(OldStream)
  100    ->  close(OldStream),
  101        retractall(stream(_))
  102    ;   true),
  103    current_month(Year, Month),
  104    bc_month_file_name(Year, Month, File),
  105    open(File, append, Stream, [encoding('utf8')]),
  106    retractall(stream(Stream)),
  107    retractall(open_month(_, _)),
  108    asserta(stream(Stream)),
  109    asserta(open_month(Year, Month)).
  110
  111bc_month_file_name(Year, Month, File):-
  112    path(Base),
  113    atomic_list_concat([Base, '/', Year, '-', Month, '.pl'], File).
  114
  115% Extracts current month.
  116
  117current_month(Year, Month):-
  118    get_time(TimeStamp),
  119    stamp_date_time(TimeStamp, DateTime, 'UTC'),
  120    date_time_value(year, DateTime, Year),
  121    date_time_value(month, DateTime, Month).
  122
  123% Writes given term in canonical form that makes
  124% it possible to read back.
  125
  126write_canonical_term(Stream, Term):-
  127    write_term(Stream, Term, [
  128        ignore_ops,
  129        quoted,
  130        dotlists(true),
  131        nl(true),
  132        fullstop(true)
  133    ]).
  134
  135% Enables analytics and sets the directory where
  136% to store there analytics logs.
  137
  138bc_enable_analytics(_):-
  139    enabled, !.
  140
  141bc_enable_analytics(Path):-
  142    debug(bc_analytics, 'Enabling analytics with storage at ~w.', [Path]),
  143    (   exists_directory(Path)
  144    ->  (   access_file(Path, write)
  145        ->  true
  146        ;   throw(error(analytics_directory_not_writable)))
  147    ;   throw(error(analytics_directory_not_exists))),
  148    asserta(enabled),
  149    asserta(path(Path)).
  150
  151% Flushes the log stream.
  152
  153bc_analytics_flush_output:-
  154    (   stream(Stream)
  155    ->  flush_output(Stream)
  156    ;   true)