Record the import range start when an import begins, CLOPS-1053 - #655
Merged
Merged
Conversation
Imports run backwards, from the end date towards the oldest date there is data for. Both finishing an import and switching over to importing recent days compare progress against the recorded import_range_start, but startingImport() never set it, so imports created by makeSite() - which is every import started from the Cloud "connect with Google" flow - carried a null range start for their whole life. Once such an import walked back to the oldest date it could neither finish nor move forward: each scheduled run recomputed the range, found nothing to do and exited in about a second, leaving the status on "ongoing" and the admin page reporting it as terminated with whatever error had last been recorded. Recent data was never imported. Set the range start from the site creation date, which both importers take from the GA property's create time immediately before starting the import, and which the import command already falls back to on every run. Also stop setImportDateRange() blanking the range start when no start date is passed - Controller::startImportGA4() calls it that way whenever only an end date was given, which would otherwise drop the value straight after it was set. No migration: this fixes new imports only. Existing statuses with a null range start need their range start backfilled, and where last_day_archived has drifted later than main_import_progress that needs realigning too, otherwise the import is marked finished without importing recent data.
AltamashShaikh
force-pushed
the
CLOPS-1053-fix-stalled-ga-imports
branch
from
September 11, 2026 08:32
e28df85 to
e740039
Compare
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.
Description
GA imports run backwards: they start at the end date and walk back towards the oldest date there is data for. That oldest date is held in the import status as
import_range_start, and two separate pieces of logic compare progress against it —finishImportIfNothingLeft(), which marks an import complete, and the branch inImportGA4Reportsthat switches an import over to pulling recent days once the backward walk is done.startingImport()never set it. It wrote'import_range_start' => nulland left it to whoever started the import to fill in afterwards.Controller::startImportGA4()does that when the user supplies dates, butImporterGA4::makeSite()does not — and that is the path taken by every import created through the Matomo Cloud "connect with Google" flow, as well as any UI import started without an explicit date range.Those imports ran normally for as long as they had days left to fetch. The failure only appeared when one reached the oldest date, at which point it could neither finish nor move forward:
finishImportIfNothingLeft()bails on!empty($status['import_range_start']), so the status stayedongoingforever.last_day_archived === import_range_start, which can never match null, so it never fired.The result is an import that the hourly task relaunches every hour, that recomputes its range, finds nothing to do, and exits after about a second — while recent data is never imported. Because the status stays
ongoingand the job is not running, the admin page derives the "Terminated" label for it, and prints alongside it whatever was last written to$status['error'](nothing ever clears that field). That made a stalled-but-otherwise-healthy import present as a credentials failure.This sets
import_range_startfrom the site creation date, which both importers take from the GA property'sgetCreateTime()immediately before starting the import, and whichImportGA4Reportsalready uses as its own per-run fallback. So the value is not new — it is the one the importer was already deriving each run, now recorded once where the rest of the code expects to find it. The lookup is guarded: a missing or unreadable creation date falls back to the previousnullrather than failing the import start.It also stops
setImportDateRange()blanking the range start when passed a null start date.Controller::startImportGA4()calls it that way whenever either date is given, so an import started with only an end date would have had its range start dropped immediately afterstartingImport()set it. Callers passing no start date mean "leave it alone", not "clear it" —changeImportEndDate()already depends on that by reading the existing value and passing it straight back.Deliberately not included
No migration. Existing statuses with a null range start are not repaired by this PR, so they stay stalled until backfilled. That was a considered call: a migration here has to write to every GA import status row on every instance,
Site::getCreationDateFor()throws on deleted sites (orphaned status rows demonstrably exist —enrichStatus()already catchesUnexpectedWebsiteFoundExceptionfor exactly that), and a throwing migration blocks the whole Matomo update, not just this plugin.There is also a case a migration would have to get right. Where
last_day_archivedhas drifted later thanmain_import_progress, filling in onlyimport_range_startmakesfinishImportIfNothingLeft()pass while the forward-catch-up branch still does not fire — so the import is silently markedfinishedwith recent data never imported. That is worse than leaving it stalled, becauseresumeScheduledImportsskips finished imports entirely and it cannot self-correct.Affected instances can be found with a read-only query, and repaired with a targeted update:
Set
import_range_startto the site creation date, and wherelast_day_archivedis later thanmain_import_progress, lower it to match. That is what was applied to the three sites in the originating report, and they resumed importing the missing window immediately.Worth filing separately, both found while tracing this:
resumeImport()never clears$status['error'], so a long-resolved error is replayed against every later termination; and the guard instartingImport()throwsCancelExistingImportFirstinside atrywhose owncatchswallows it, so an in-progress import is always overwritten instead of refused.Issue No
CLOPS-1053
Steps to Replicate the Issue
ImporterGA4::makeSite()directly), and let it run until it has imported back to the GA property's first day. The stored status has"import_range_start":nullandmain_import_progressequal to the site creation date.(Entry #0) is finished, moving on.and[0 API requests made to GA], the status staysongoing, no recent data is imported, and the admin page shows the import as "Terminated" with a stale error message beneath it.Checklist
Verified locally:
ImportStatusTest60/60 andTasksTest6/6 green, PHPCS clean againstphpcs.xml, PHPStan level 1 clean.