Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
87 / 87
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
TrackerService
100.00% covered (success)
100.00%
87 / 87
100.00% covered (success)
100.00%
8 / 8
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVisitId
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getCurrentSession
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getCurrentSessionByConfigId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 doReturningVisit
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 writeEvent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 updateVisit
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 generateConfigId
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Drupal\visitors\Service;
4
5use Drupal\Core\Database\Connection;
6use Drupal\visitors\VisitorsTrackerInterface;
7
8/**
9 * Tracker for web analytics.
10 */
11class TrackerService implements VisitorsTrackerInterface {
12
13  /**
14   * The database connection.
15   *
16   * @var \Drupal\Core\Database\Connection
17   */
18  protected $database;
19
20  /**
21   * Tracks visits and events.
22   *
23   * @param \Drupal\Core\Database\Connection $database
24   *   The database connection.
25   */
26  public function __construct(Connection $database) {
27    $this->database = $database;
28  }
29
30  /**
31   * {@inheritdoc}
32   */
33  public function getVisitId(array $fields, int $request_time): int {
34
35    $fields['config_id'] = $this->generateConfigId($fields);
36
37    $ago = $request_time - VisitorsTrackerInterface::SESSION_TIMEOUT;
38
39    $id = $this->getCurrentSession($fields, $ago);
40    if ($id) {
41      return $id;
42    }
43
44    $this->doReturningVisit($fields);
45
46    $id = $this->database->insert('visitors_visit')
47      ->fields($fields)
48      ->execute();
49
50    return $id;
51  }
52
53  /**
54   * Get the current session id.
55   *
56   * @param array $fields
57   *   The fields to get the session id from.
58   * @param int $ago
59   *   The time to check for the session id.
60   */
61  protected function getCurrentSession(array $fields, int $ago): ?int {
62
63    if (empty($fields['visitor_id'])) {
64      return $this->getCurrentSessionByConfigId($fields, $ago);
65    }
66
67    $current_session = $this->database->select('visitors_visit', 'vv');
68    $current_session->fields('vv', ['id']);
69    $current_session->condition('vv.visitor_id', $fields['visitor_id']);
70    $current_session->condition('vv.exit_time', $ago, '>');
71
72    $id = $current_session->execute()->fetchField();
73
74    return $id;
75  }
76
77  /**
78   * Get the current session id by config id.
79   *
80   * @param array $fields
81   *   The fields to get the session id from.
82   * @param int $ago
83   *   The time to check for the session id.
84   */
85  protected function getCurrentSessionByConfigId(array $fields, int $ago): ?int {
86    if (empty($fields['config_id'])) {
87      return NULL;
88    }
89
90    $current_session = $this->database->select('visitors_visit', 'vv');
91    $current_session->fields('vv', ['id']);
92    $current_session->condition('vv.config_id', $fields['config_id']);
93    $current_session->condition('vv.location_ip', $fields['location_ip']);
94    $current_session->condition('vv.exit_time', $ago, '>');
95
96    $id = $current_session->execute()->fetchField();
97
98    return $id;
99  }
100
101  /**
102   * Check if the visitor has visited before.
103   *
104   * If the visitor has visited before, update the visit count and set the
105   * returning flag.
106   *
107   * @param array $fields
108   *   The fields to check for the visitor.
109   */
110  protected function doReturningVisit(&$fields): void {
111    if (empty($fields['visitor_id'])) {
112      return;
113    }
114
115    $returning_visitor = $this->database->select('visitors_visit', 'vv2');
116    $returning_visitor->fields('vv2', ['total_visits', 'entry_time', 'exit_time', 'time_since_first'])
117      ->condition('vv2.visitor_id', $fields['visitor_id'])
118      ->orderBy('vv2.exit_time', 'DESC')
119      ->range(0, 1);
120
121    $result = $returning_visitor->execute()->fetchAssoc();
122
123    if ($result !== FALSE) {
124      $fields['total_visits'] = $result['total_visits'] + 1;
125      $fields['returning'] = 1;
126
127      // Calculate time since last visit (seconds)
128      $fields['time_since_last'] = $fields['entry_time'] - $result['exit_time'];
129
130      // Calculate time since first visit by adding the time between visits
131      // to the previous visit's time_since_first value.
132      $fields['time_since_first'] = $result['time_since_first'] + ($fields['entry_time'] - $result['entry_time']);
133    }
134    else {
135      // New visitor - no previous visits.
136      $fields['total_visits'] = 1;
137      $fields['returning'] = 0;
138      $fields['time_since_first'] = 0;
139      $fields['time_since_last'] = 0;
140    }
141  }
142
143  /**
144   * {@inheritdoc}
145   */
146  public function writeEvent(array $fields): int {
147    $id = $this->database->insert('visitors_event')
148      ->fields($fields)
149      ->execute();
150
151    return $id;
152  }
153
154  /**
155   * {@inheritdoc}
156   */
157  public function updateVisit(int $visit_id, int $event_id, int $exit_time, ?int $uid) {
158
159    $update = $this->database->update('visitors_visit');
160    $update->fields([
161      'exit_time' => $exit_time,
162      'exit' => $event_id,
163    ])
164      ->expression('total_time', ':now - entry_time', [':now' => $exit_time])
165      ->expression(
166        'total_page_views',
167        '(SELECT COUNT(DISTINCT page_view) FROM {visitors_event} ve WHERE ve.visit_id = :id)',
168        [':id' => $visit_id]
169      )
170      ->expression(
171        'total_events',
172        '(SELECT COUNT(id) FROM {visitors_event} ve WHERE ve.visit_id = :id)',
173        [':id' => $visit_id],
174        )
175      ->expression('entry', 'COALESCE(entry, :event_id)', [':event_id' => $event_id])
176      ->expression('uid', 'CASE WHEN uid > 0 THEN uid ELSE :uid END', [':uid' => $uid])
177      ->condition('id', $visit_id)
178      ->execute();
179
180  }
181
182  /**
183   * Generate a config id.
184   *
185   * @param array $fields
186   *   The fields to generate the config id from.
187   *
188   * @return string
189   *   The generated config id 16 characters.
190   */
191  protected function generateConfigId($fields) {
192    $os = $fields['bot'] ? 'bot' : $fields['config_os'];
193    $config_string =
194      $os
195      . $fields['config_browser_name']
196      . $fields['config_browser_version']
197      . $fields['config_flash']
198      . $fields['config_java']
199      . $fields['config_quicktime']
200      . $fields['config_realplayer']
201      . $fields['config_pdf']
202      . $fields['config_windowsmedia']
203      . $fields['config_silverlight']
204      . $fields['config_cookie']
205      . $fields['config_resolution']
206      . $fields['location_browser_lang'];
207
208    $sha = hash('sha256', $config_string);
209    $config_id = substr($sha, 0, 16);
210
211    return $config_id;
212  }
213
214}