Stock GLPI has no read record: the ticket history keeps what changed, never who simply opened and read it. In our work maintaining client GLPI estates, this becomes the classic standoff of "the technician never even looked at the ticket" versus "yes, I read it" - and with no objective data it is one person's word against the other's. The free Ticket Tracker module closes that gap by logging every ticket view. This guide shows how it stores the data, how to configure it without inflating counters, and three SQL queries that answer "who read what" on the spot.
Why GLPI does not know who read a ticket
The native history (glpi_logs) only records changes: creation, status change, new followup, technician reassignment. Opening the form and reading it changes no field, so it produces no row at all. There is no "view" event in the GLPI core. Ticket Tracker solves this by hooking into the moment the ticket form is rendered (the Ticket postShowItem hook): every time an authenticated session displays a ticket, the module logs the view. No manual action, no new field on the form.
The two storage layers: summary and log
The module writes to two tables with distinct purposes. The summary answers "how many times and when"; the log answers "each access, one by one":
-- Summary per (ticket, user): one row, updated on each access.
CREATE TABLE `glpi_plugin_nextool_tickettracker_views` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`tickets_id` int unsigned NOT NULL,
`users_id` int unsigned NOT NULL,
`view_count` int unsigned NOT NULL DEFAULT 1,
`first_view` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_view` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_ticket_user` (`tickets_id`, `users_id`)
);
-- Detailed log: one row per individual access.
CREATE TABLE `glpi_plugin_nextool_tickettracker_viewlog` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`tickets_id` int unsigned NOT NULL,
`users_id` int unsigned NOT NULL,
`view_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
The UNIQUE key (tickets_id, users_id) is the heart of the summary table: each ticket/user pair has exactly one row. Writes use INSERT ... ON DUPLICATE KEY UPDATE view_count = view_count + 1, an atomic upsert - two simultaneous accesses by the same user neither create a duplicate row nor lose a count. The "Views" tab on the ticket reads the summary (a table with all viewers, count, first and last read) and offers an expandable cascade that pulls each individual access from the log, with date and time.
Activation and configuration checklist
The module is free and turns on from the NexTool catalog. Once enabled, the settings live under Modules > Ticket Tracker, Features tab:
- Module active - the same toggle as on the Modules page. Off, nothing is tracked.
- Tracking active (tracking_enabled) - turns on view logging. It can stay on with the tab hidden if you want to collect without exposing.
- Show tab on ticket (show_tab_on_ticket) - shows or hides the "Views" tab on the ticket form.
- Minimum interval (s) (min_interval_seconds, default 60) - a debounce window: accesses by the same user to the same ticket within that interval count as one.
- Ignored (Additional permissions tab) - users, profiles or groups that view without being tracked. All blank = every access counts.
The field mistake that inflates (or zeroes) the counters
The common mistake is zeroing the minimum interval, thinking "the more granular, the better". The hook fires on every render of the form - including reloads, back navigation and reopening the tab. With min_interval_seconds = 0, a technician who leaves the ticket open and hits refresh three times becomes "three reads", and view_count loses its meaning of "how many times they actually came back to the matter". The 60-second default exists precisely to collapse the burst of reloads into a single read. Use 0 only for a one-off diagnostic and return to the default afterwards.
The second mistake is subtler and we hit it at a client: up to module version 1.2.0, logging depended on permission to see the Views tab - meaning only administrative profiles were counted. The requester and the observers opened the ticket and nothing was logged, so the data under-reported exactly the people who matter most in an SLA dispute. We decoupled logging from the tab-view permission: today any authenticated session that renders the ticket is counted - requester, observer or technician - regardless of profile. If you run an older version, update before trusting the numbers.
Decision matrix: interval and ignored list by scenario
The right setting depends on what you want to prove. The matrix we use during design:
| Operating scenario | Tracking | Minimum interval (s) | Who to put in "ignored" |
|---|---|---|---|
| Read audit / SLA (default) | On | 60 | Nobody |
| Estate with auto-refresh or wall dashboard | On | 180 to 300 | Integration accounts / bots |
| Supervision that must not inflate the counter | On | 60 | Manager / auditor profile |
| One-off diagnostic (count every access) | On | 0 (temporary) | Nobody |
Three SQL queries that answer on the spot
The tab handles the case by case; for an estate-wide view, go straight to the database. Open assigned tickets the responsible technician has not opened yet (type = 2 in glpi_tickets_users is the assigned technician):
-- Open assigned tickets the responsible technician has NEVER viewed.
SELECT t.id AS ticket, t.name AS title, u.name AS technician, t.date AS opened_at
FROM glpi_tickets t
JOIN glpi_tickets_users tu ON tu.tickets_id = t.id AND tu.type = 2
JOIN glpi_users u ON u.id = tu.users_id
LEFT JOIN glpi_plugin_nextool_tickettracker_views v
ON v.tickets_id = t.id AND v.users_id = tu.users_id
WHERE t.is_deleted = 0
AND t.status IN (1, 2, 3, 4) -- new, assigned, planned, pending
AND v.id IS NULL -- no read logged by the technician
ORDER BY t.date ASC;
Read latency - how many minutes the ticket stayed open until the assigned technician's first read. It is the metric that precedes the first reply and often explains a breached SLA:
-- Minutes between opening and the 1st read by the assigned technician.
SELECT t.id AS ticket,
TIMESTAMPDIFF(MINUTE, t.date, v.first_view) AS minutes_to_first_read
FROM glpi_tickets t
JOIN glpi_tickets_users tu ON tu.tickets_id = t.id AND tu.type = 2
JOIN glpi_plugin_nextool_tickettracker_views v
ON v.tickets_id = t.id AND v.users_id = tu.users_id
WHERE t.is_deleted = 0
ORDER BY minutes_to_first_read DESC;
The most active viewers of a specific ticket, from the summary, to see who actually follows it (replace 12345 with the ticket ID):
-- Who opened ticket 12345 the most, from most recurrent to least.
SELECT u.name AS username, v.view_count, v.first_view, v.last_view
FROM glpi_plugin_nextool_tickettracker_views v
JOIN glpi_users u ON u.id = v.users_id
WHERE v.tickets_id = 12345
ORDER BY v.view_count DESC;
A read is not a reply: what the counter proves
The data is powerful, but it has an honest limit: it proves the form was rendered for that user, not that the person read it carefully or acted on it. A high view_count with no followup is a yellow flag - someone is opening and closing without resolving. A ticket with zero views from the owner while the SLA runs is a red flag - nobody has even looked. In sustainment, the real value is not policing technicians, it is closing the evidentiary gap: when the client asks "why did this ticket sit for three days?", the answer stops being an opinion and gains a date-and-time stamp.
Need real visibility over service delivery in your GLPI, with the right modules enabled and tuned to your workflow? Discover NexTool's GLPI sustainment service.
This content was produced with the assistance of artificial intelligence and reviewed by the Nextool Solutions team.