Conversation
Replace the derived "delta to next log_link_visit_action row" time-on-page calculation with a dedicated per-pageview row in `log_page_view_time`, keyed on `(idvisit, idpageview)` so each browser tab accumulates its own time. Events, heartbeats, and content interactions now update the tab's row instead of stealing time from the following pageview, and the visit's final pageview no longer defaults to 0s. Guarded by `[Tracker] record_accurate_page_view_time = 1` so 5.x can fall back to the legacy `time_spent_ref_action` query if needed. The archiver also probes `log_page_view_time` for the archive period before dispatching so re-archiving a pre-upgrade day (or archiving days from before the kill-switch flip) keeps the legacy sum_time_spent β mixed-schema states never silently collapse to zero. Includes: - core/Updates/5.13.0-b1.php migration creating log_page_view_time (unique (idvisit, idpageview), index on (idvisit, idaction_url) and (idsite, server_time)). - plugins/Actions/Tracker/PageViewTimeWriter β single-statement INSERT + close-previous-pageview UPDATE + same-tab UPDATE; visit_standard_length cap applied inline via LEAST/GREATEST. - plugins/Actions/RecordBuilders/ActionReports β dual-path archiver dispatch (accurate + legacy) with the mid-week / invalidation safety probe described above. - plugins/Actions/Columns/Metrics/AverageTimeOnPage β divides by nb_hits_with_time_spent when present, falls back to nb_hits so pre-upgrade archives stay correct. - plugins/Actions/VisitorDetails β LEFT JOIN log_page_view_time and prefer its time_spent for the per-pageview visitor log field. - Integration tests: 9 writer scenarios + 2 archiver scenarios covering the mid-week kill-switch flip (weekly archive sums both halves) and pre-upgrade-day invalidation (re-archive keeps the legacy numbers).
caddoo
added a commit
that referenced
this pull request
Jul 8, 2026
Ports the intended output changes from PR #24788 (DEV-20451) into the in-tree plugin expected fixtures: - Actions.getPageUrls / Actions.getPageTitles fixtures for PagePerformance and CustomVariables (segment variants) now include the new `nb_hits_with_time_spent` element and reflect the accurate archive path values. - BotTracking real-time page-URL and chatbot reports pick up the same new element via the Actions record it embeds. - API AutoSuggestAPITest's Live.getLastVisitsDetails_range fixture and the four Ecommerce Live.getLastVisitsDetails_day fixtures now match the VisitorDetails join surfacing accurate `pageTimeSpent` for pageviews. - PrivacyManager exportDataSubject_* JSON fixtures now include the new `log_page_view_time` section (the LogTable class declares hasIdVisitorColumn() = true so the anonymizer/exporter sweeps it). CustomVariables and Bandwidth are git submodules β their fixture/screenshot updates will land via separate submodule PRs.
Squashed collection of every downstream test-fixture / assertion update
required by the DEV-20451 change:
- ~260 system-test expected XML/JSON fixtures under
`plugins/*/tests/System/expected/` and
`tests/PHPUnit/System/expected/` β synced from CI runs. All diffs
fall into a small set of intended shapes: new
`<nb_hits_with_time_spent>` element on Actions.getPageUrls /
Actions.getPageTitles / getEntry* / getExit*; shifted
`<sum_time_spent>` / `<avg_time_on_page>` values (accurate archive
path with visit_last_action_time backfill); shifted `<timeSpent>` in
Live.getLastVisitsDetails (VisitorDetails now prefers
pageTimeSpent); new `<min>` / `<max>` / `<change>` metadata on
Actions.getRowEvolution now that avg_time_on_page has non-zero
variation to report; and a new `log_page_view_time` section in
PrivacyManager exportDataSubject_* JSON fixtures.
- Six `tests/PHPUnit/System/expected/test_TimezonesTest_*__Live.
getLastVisitsDetails_*.xml` fixtures stripped of stale `<idVisit>N
</idVisit>` values that were carried over from a pre-`xmlFieldsToRemove`
regeneration.
- 11 in-tree UI expected-screenshots (Overlay Γ8, Live Γ1,
Installation Γ1, UIIntegrationTest Γ1) resynced to reflect the new
metric values in the Pages / Overlay sidebar / visitor profile /
Diagnostics config file / Installation existing-tables list.
- Integration-test assertion updates:
* PrivacyManager DataPurgingTest β `log_page_view_time => 6` added
to each `$expectedPrediction` block (all 8).
* PrivacyManager DataSubjectsTest β `'log_page_view_time' => N`
added to six `assertEquals` blocks (counts 4 / 4 / 3 / 144 / 144
/ 72 per fixture).
* PrivacyManager LogDataAnonymizerTest β 4
anonymizeVisitInformation_*.json fixtures regenerated to include
the new log_page_view_time section.
* core MetricsTest β added `'nb_hits_with_time_spent' => 45` to
the id-to-name mapping expected array.
* core RawLogDaoTest β added `log_page_view_time => idpageviewtime`
+ max-id `'11'` entries in the correct key order.
* BackwardsCompatibility1XTest β added `'nb_hits_with_time_spent'`
to `xmlFieldsToRemove`; the 1.X SQL dump doesn't populate
log_page_view_time so the archiver correctly falls back to the
legacy path, and stripping the field keeps the shared-fixture
comparison stable across both archiving paths.
CustomVariables, AnonymousPiwikUsageMeasurement, DeviceDetectorCache,
MarketingCampaignsReporting, TagManager and Bandwidth are git
submodules β their fixture/screenshot updates land via separate
submodule PRs.
# Conflicts: # core/Version.php
This was referenced Jul 9, 2026
- plugin-AnonymousPiwikUsageMeasurement#108 - plugin-Bandwidth#112 - plugin-CustomVariables#133 - plugin-DeviceDetectorCache#88 - plugin-MarketingCampaignsReporting#222 Each submodule PR carries only the expected-fixture / screenshot updates required by the new accurate per-pageview time-on-page metric.
β¦ollow-up fixture syncs)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
log_link_visit_actionrow" time-on-page calculation with a dedicated per-pageview row in a newlog_page_view_timetable, keyed on(idvisit, idpageview)so each tab (pv_id) accumulates its own time.visit_last_action_timeat archive time instead of defaulting to 0 s.Tracker.record_accurate_page_view_time = 1kill-switch (temporary, planned for removal in Matomo 6.0). The archiver also probeslog_page_view_timefor the current archive period and falls back to the legacytime_spent_ref_actionquery when no rows exist β so mixed-schema states (mid-week upgrade, invalidation of pre-upgrade days) never silently collapsesum_time_spentto zero.nb_hits_with_time_spentmetric mirrors the existingnb_hits_with_time_generationconvention;AverageTimeOnPagedivides by it when present and falls back tonb_hitson pre-upgrade archives.Related tickets
Reference
PoC branch:
dev-15222. This PR ports the PoC minus thepv_timeclient-time tracker parameter (to be handled in a follow-up ticket) and adds a presence-check safeguard in the archiver so re-archiving of pre-upgrade days stays on the legacy metric.Performance
Two write layers add work on the hot tracker path: the previous-pageview close on a new
TYPE_PAGE_URL, and the same-tab UPDATE for every follow-up hit (event, heartbeat, content, search). Both were measured on a seeded dataset of ~4 Mlog_page_view_timerows across ~500 k visits (single MySQL 5.7 instance, no tuning). Full report + EXPLAIN output at/tmp/dev20451-perf/REPORT.mdlocally; the highlights:Per-hit tracker cost
ORDER BY id DESC LIMIT 1)index_idvisit_idaction_url, 1 rowWHERE idvisit=? AND idpageview=?)unique_idvisit_idpageviewEvery writer query is a single-row indexed operation. There are no full scans, no
SELECTinside the writer path, and no session variables. Thevisit_standard_lengthcap is applied inline viaLEAST(cap, GREATEST(time_spent, elapsed))so a stalled tab cannot inflate a row beyond the visit cap.Archive-time cost
The accurate archiver examines ~37 % fewer rows per day than the legacy path (1.94 M vs 3.06 M on the seeded day), because
log_page_view_timeholds one row per pageview whereaslog_link_visit_actionholds one row per hit. Query cost fromEXPLAIN FORMAT=JSONis 617 k for the accurate path vs 735 k for legacy β comparable, with the row-count advantage offsetting the extralog_visitJOIN.The archiver-side presence check (
hasAccuratePageViewTimeRowsInPeriod) is covered byindex_idsite_server_time(using_index=true, query cost 1.41) β it's essentially free per archive-day-per-site.Storage cost
log_page_view_timeaverages ~178 B/row on disk including indices. Projected footprint: ~1.8 GB per 10 M pageviews. Row-purger integration is automatic:PageViewTime::hasIdVisitorColumn() = true+getDateTimeColumn() = 'server_time', so existingRawLogDaosweeps pick it up alongsidelog_link_visit_action.Comparison to the older PoC (
dev-18703-write-time-spent)The earlier PoC took a different, simpler shape β worth comparing so the tradeoffs are explicit:
dev-18703DEV-20451)(idvisit, idsite, servertime)(idvisit, idpageview)withUNIQUEindexpv_idSELECT MAX(servertime) INTO @var; UPDATE ... WHERE servertime=@var) plusINSERT IGNOREon new PVUPDATEorINSERT ... ON DUPLICATE KEY UPDATE)@max_servertimeβ needed the follow-up Aurora fixSELECT then UPDATEwindow; another connection can insert a new row between the two statementsGREATEST()so out-of-order pings only growtime_spent = now - servertime, unboundedLEAST(visit_standard_length, ...)applied inlineWHERE idvisit=? AND idsite=?β depends on theMAX(servertime)scanservertimeused as effective key)core/Updates/5.13.0-b1.php+ fresh-install schemaConcurrency prediction under real tracking load β the two most concurrency-sensitive scenarios are:
dev-18703runsSELECT MAX(servertime)twice under lock; one connection may see stale data before the other's UPDATE lands. This PR uses a singleUPDATE ... WHERE idvisit=? AND idpageview=?β InnoDB serializes the two on the same row lock,GREATESTguarantees the value only grows, no lost update.dev-18703uses a single visit-level row keyed on(idvisit, servertime)β the row is contested and one tab's time may overwrite the other. This PR keys per tab ((idvisit, idpageview)) so the two writes hit distinct rows. Zero contention between tabs.Deadlock risk: none observed in local writer tests (concurrent multi-tab scenarios covered by the writer test suite pass consistently). Because every writer path targets exactly one row via a unique index, and no writer path takes locks on more than one row, the classic two-row cross-lock pattern that produces deadlocks cannot arise. The archiver reads with
READ COMMITTEDsemantics viaLogAggregator, so it doesn't take any row locks the writer would wait on.What this PR still doesn't answer for performance
pv_timetracker parameter (deferred to a follow-up ticket) will add one more integer per tracker request. It is not measured here.Why screenshots changed
CI produced 14 failed UI screenshots. Inspected all of them via the processed artifacts. They fall into three groups, each expected:
Group A β Time-on-page numbers moved (12 screenshots)
Bandwidth_actions_page_urls,Bandwidth_actions_page_urls_flat,Bandwidth_actions_page_titles,Live_visitor_profile_action_tooltip,Overlay_loaded,Overlay_page_link_clicks,Overlay_page_new_links,Overlay_page_change,Overlay_period_change,Overlay_row_evolution,Overlay_transitions,Overlay_loaded_from_actions.Direct consequence of the metric change. The
Avg. time on pagecolumn in the Pages report and the "Time on page" field in the Overlay sidebar / visitor-profile tooltip now come fromsum_time_spent / nb_hits_with_time_spentinstead ofsum_time_spent / nb_hits, so the displayed durations shift. E.g. Bandwidth Pages report rowtest: 00:00:24 β 00:02:07; Overlay sidebar forpage-1:36s Avg. time on pagewhere the legacy render showed a smaller value. Nothing else on these pages changed.Group B β New config key in the rendered config file (1 screenshot)
UIIntegrationTest_admin_diagnostics_configfileβ the System β Diagnostics config-file view dumpsconfig/global.ini.phpverbatim, so the new[Tracker] record_accurate_page_view_time = 1line now appears in the screenshot.Group C β New table in the schema (1 screenshot)
Installation_db_existingβ the Installation "Some tables already exist" screen enumerates everymatomo_*table found in the target DB.matomo_log_page_view_timenow appears in that list because the migration creates it.All three groups are the intended output of the PR; no unintended regressions were spotted in the pixels. Expected screenshots need to be resynced β the Bandwidth ones live in the
plugin-Bandwidthsubmodule; Overlay / Live / Installation / UIIntegrationTest are in-tree.Why some TimezonesTest fixtures dropped
<idVisit>Six
tests/PHPUnit/System/expected/test_TimezonesTest_*__Live.getLastVisitsDetails_*.xmlfixtures lost every<idVisit>N</idVisit>element in this PR's sync commit. This is not a regression β nothing on this branch changed howidVisitis emitted.tests/PHPUnit/System/TimezonesTest.phpexplicitly declares:on each of the six Live.getLastVisitsDetails testcases. The Matomo test framework strips
<idVisit>from the API response before comparing against the expected fixture, precisely because visit-ID numbering is flaky under the timezone permutations this test runs.The old checked-in fixtures still had
<idVisit>67</idVisit>etc. populated β thexmlFieldsToRemoverule was added to TimezonesTest after those files were last regenerated. CI on this branch ran the test, applied the removal rule, and produced a "clean" processed file.development:sync-system-test-processedcopied that clean file over the stale one, so the diff looks like a data loss but is actually a long-overdue catch-up. The test framework has always been ignoring these values at assertion time, so no runtime behaviour changes.Checklist