You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We're moving the activity timeline onto PaperTrail (see PR #2245). PaperTrail versions User account changes, but three of Ahoy's semantic auth.* events don't survive the move β they change skipped columns or bypass PaperTrail entirely, so no version is written at all:
Ahoy event
Trigger
Why no version
auth.password_changed
encrypted_password change
encrypted_password is in has_paper_trail skip: (secret)
auth.password_first_set
encrypted_password (welcome flow)
same skip
auth.account_setup_completed
welcome_instructions_token cleared
done via update_columns β bypasses callbacks/PaperTrail
Separately, the column-backed auth events (login, admin granted/revoked, locked/unlocked, deactivated/reactivated, email changed/confirmed, reset sent, welcome sent, account deleted) do produce versions β but as raw column diffs (super_user: false β true), not the semantic label Ahoy emitted.
Do we have the timestamps we'd need? No.
Present *_at columns cover most events, but two are missing:
password_changed_at β needed for password_changed / password_first_set
setup_completed_at (or welcome_completed_at) β needed for account_setup_completed
Proposed plan
Add non-secret timestamp columnspassword_changed_at and setup_completed_at, stamped whenever those events happen. Stamping them creates a real (non-secret) version, restoring the three lost events. Fix the update_columns bypass so the setup-completion write goes through a normal update (or explicitly stamp the timestamp).
Semantic labels for the timeline. Two options β decide in this issue:
(a) Derive in the timeline presenter from the version's changeset (e.g. super_user trueβfalse β "Admin revoked"). No schema change; labeling logic lives in one place with the rest of the timeline rendering.
(b) action_note meta column on versions, populated at write time via has_paper_trail meta: (a proc that names the semantic event). Captures intent the diff can't always recover, but duplicates labeling logic and only helps User.
Recommendation: (a) for the column-backed events (the diff already says what happened), plus the new timestamps from step 1 for the three bypass cases. Reserve action_note for cases where intent genuinely isn't recoverable from the diff.
password_first_set vs password_changed is still derivable under (a): the password_changed_at transition disambiguates them β nil β T is first-set, T β T' is a change β so this pair doesn't need action_note.
Attribute the actor (whodunnit) on self-serve Devise flows. The lost/column-backed events fire in controllers that run without a logged-in user β PasswordsController and WelcomeController both skip_before_action :authenticate_user! (and confirmation/unlock link flows are the same). set_paper_trail_whodunnit reads current_user, so every version these flows write lands with nil whodunnit and the timeline's "By" column comes up blank β where Ahoy attributed the actor. Set PaperTrail.request.whodunnit to the resource itself (self-serve) in these actions, mirroring the existing updated_by: current_user || @user pattern already used in welcome_controller.rb. Without this, restoring the events isn't enough β they'd read as "by (nobody)".
Keep the User skip: list as-is (secrets stay out of the trail); the new timestamps are the non-secret signal.
Out of scope
Attachment add/remove (avatar/logo) β separate residual gap, tracked with the broader attachment question.
Analytics vs. audit: PaperTrail replaces the account change timeline only. Ahoy's page views / prints / downloads / search intent / visits, and its association + rich-text change capture, have no version equivalent and stay on Ahoy. "Retire Ahoy" is scoped to the change-audit timeline, not engagement analytics.
π€ From Claude:
Background
We're moving the activity timeline onto PaperTrail (see PR #2245). PaperTrail versions User account changes, but three of Ahoy's semantic
auth.*events don't survive the move β they change skipped columns or bypass PaperTrail entirely, so no version is written at all:auth.password_changedencrypted_passwordchangeencrypted_passwordis inhas_paper_trail skip:(secret)auth.password_first_setencrypted_password(welcome flow)auth.account_setup_completedwelcome_instructions_tokenclearedupdate_columnsβ bypasses callbacks/PaperTrailSeparately, the column-backed auth events (login, admin granted/revoked, locked/unlocked, deactivated/reactivated, email changed/confirmed, reset sent, welcome sent, account deleted) do produce versions β but as raw column diffs (
super_user: false β true), not the semantic label Ahoy emitted.Do we have the timestamps we'd need? No.
Present
*_atcolumns cover most events, but two are missing:password_changed_atβ needed forpassword_changed/password_first_setsetup_completed_at(orwelcome_completed_at) β needed foraccount_setup_completedProposed plan
password_changed_atandsetup_completed_at, stamped whenever those events happen. Stamping them creates a real (non-secret) version, restoring the three lost events. Fix theupdate_columnsbypass so the setup-completion write goes through a normalupdate(or explicitly stamp the timestamp).super_usertrueβfalse β "Admin revoked"). No schema change; labeling logic lives in one place with the rest of the timeline rendering.action_notemeta column onversions, populated at write time viahas_paper_trail meta:(a proc that names the semantic event). Captures intent the diff can't always recover, but duplicates labeling logic and only helps User.action_notefor cases where intent genuinely isn't recoverable from the diff.password_first_setvspassword_changedis still derivable under (a): thepassword_changed_attransition disambiguates them βnil β Tis first-set,T β T'is a change β so this pair doesn't needaction_note.whodunnit) on self-serve Devise flows. The lost/column-backed events fire in controllers that run without a logged-in user βPasswordsControllerandWelcomeControllerbothskip_before_action :authenticate_user!(and confirmation/unlock link flows are the same).set_paper_trail_whodunnitreadscurrent_user, so every version these flows write lands with nil whodunnit and the timeline's "By" column comes up blank β where Ahoy attributed the actor. SetPaperTrail.request.whodunnitto the resource itself (self-serve) in these actions, mirroring the existingupdated_by: current_user || @userpattern already used inwelcome_controller.rb. Without this, restoring the events isn't enough β they'd read as "by (nobody)".skip:list as-is (secrets stay out of the trail); the new timestamps are the non-secret signal.Out of scope