Skip to content

# fix(export): seconds_stayed wrong for expired unit sessions; add missing columns from webview - #703

Open
timseidel wants to merge 2 commits into
rubenarslan:masterfrom
timseidel:fix/user-detail-export-seconds-stayed
Open

timseidel wants to merge 2 commits into
rubenarslan:masterfrom
timseidel:fix/user-detail-export-seconds-stayed

Conversation

@timseidel

Copy link
Copy Markdown
Contributor

Discrepancy see here:

formr admin.pdf
esm_user_detail.csv

Liebe Grüße!
Tim

The bug

RunHelper::getUserDetailExportPdoStatement() computed the exported
seconds_stayed as:

IF(ended > 0, ended - created, NOW() - created)

A unit session that expired rather than ended has ended NULL, so
every expired row fell into the NOW() branch. The column then reported
"time since the participant entered this unit, as of the moment you clicked
Export"
rather than a duration.

Consequences:

  • The number grows on every re-export of the same session — it is not a
    property of the data.
  • It contradicts the admin web view, which selects created / ended /
    expired unmodified via getUserDetailTable() and therefore shows the
    truth.
  • It affects Survey inactivity expiry and elapsed Pause/Wait units — i.e.
    exactly the units whose timing you would export a user detail CSV to
    investigate.

Evidence

From an ESM run on a real study (CSV export + printed web view of the same
session, both attached to the issue):

unit session unit entered expired real duration exported seconds_stayed
1763043 Wait (50) 13:49:51 13:49:52 1 s 424
1762979 Survey (100) 13:34:56 13:36:08 72 s 1319
1763003 Survey (100) 13:39:32 13:40:33 61 s 1043
1763032 Survey (100) 13:47:38 13:48:50 72 s 557

All nine expired rows in that export resolve to the same wall-clock instant:
entered + seconds_stayed == 13:56:55 for every one of them — the moment the
export ran. That is the fingerprint of the NOW() branch. The web view for
the same rows correctly shows "a moment" / "1 minute".

The fix

Fall back to expired before NOW():

UNIX_TIMESTAMP(
    CASE
        WHEN ended   > 0 THEN ended
        WHEN expired > 0 THEN expired
        ELSE NOW()
    END
) - UNIX_TIMESTAMP(created) AS 'seconds_stayed'

NOW() now applies only to sessions that are still genuinely open (neither
ended nor expired), where "elapsed so far" is the intended meaning. Rows with
ended set are unaffected — no change to existing correct output.

Also: four columns added to the export

The CSV carried only created / ended / expired, while
templates/admin/run/user_detail.php additionally renders expires (bolded
while queued > 0) and result with result_log as its tooltip.

That gap made this class of bug undiagnosable from an export alone: a
Pause/Wait's computed deadline lives only in expires, so an export showing
a 10-minute Wait that ended after 1 second gives no way to tell whether the
deadline was miscomputed or a correct deadline was ignored. Answering that
question here required a PDF print of the web view.

expires, queued, result, result_log are appended after the existing
nine columns, so positional parsers of the old format keep working.

timseidel and others added 2 commits August 9, 2026 14:16
RunHelper::getUserDetailExportPdoStatement() computed seconds_stayed as
IF(ended > 0, ended - created, NOW() - created). A unit session that
expired rather than ended has `ended` NULL, so every expired row fell
into the NOW() branch and reported "time since the participant entered
this unit, as of the moment of export" instead of a duration.

The value therefore grew on every re-export and disagreed with the raw
timestamps rendered by the admin web view, which selects
created/ended/expired unmodified via getUserDetailTable().

Observed on an ESM run: a Wait that actually lasted 1 s exported as
424 s, and all nine expired rows in the CSV resolved to the same
wall-clock instant (entered + seconds_stayed == export time).

Fall back to `expired` before NOW(), so NOW() applies only to sessions
that are still genuinely open. Rows with `ended` set are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CSV export carried only created/ended/expired, while the admin web
view (templates/admin/run/user_detail.php) also renders `expires` —
bolded while `queued > 0` — and `result` with `result_log` as tooltip.

That gap made a whole class of bug undiagnosable from an export. A
Pause/Wait's computed deadline lives only in `expires`, so an export
showing a 10-minute Wait that ended after 1 second gave no way to tell
whether the deadline was miscomputed or a correct deadline was ignored.
Answering that required a PDF print of the web view.

Columns are appended after the existing nine so positional parsers of
the old format keep working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant