Skip to content

Attribute admin-sent emails to the sending person (invites, reminders, resends) + show From - #2089

Open
maebeale wants to merge 3 commits into
mainfrom
maebeale/bulk-invite-sender-attribution
Open

Attribute admin-sent emails to the sending person (invites, reminders, resends) + show From#2089
maebeale wants to merge 3 commits into
mainfrom
maebeale/bulk-invite-sender-attribution

Conversation

@maebeale

@maebeale maebeale commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 3 Read 📖 sender attribution threaded through several admin-triggered send paths; one shared DeviseMailer/CreateNotification change + a show-page field

Why

  • Admin-triggered emails were recorded with no sender, so the communications index/show labeled them "AWBW Portal" as if the portal sent them automatically. Attribute them to the acting person instead. A person's name should appear whenever a staff member sent it; "AWBW Portal" only for truly automated messages.

What

  • Invites — the sender rides on the User record: send_confirmation_instructions(sender:) exposes confirmation_sender, which DeviseMailer reads when it logs the notification. Devise sends with deliver_now, so the mailer holds the same instance the caller does. CreateNotification accepts sender:.
  • Event reminders — the bulk reminders page (events#send_reminder) now passes sender: current_user per recipient.
  • Resendsnotifications#resend attributes the resent copy to the admin who resent it.
  • Show page — added a From row, backed by NotificationDecorator#sender_name (also used by the index and the notification row, which previously disagreed on capitalization).

Tests

  • Reminders: each notification's sender is the admin, and no sender_id is nil (guards the "AWBW Portal" regression).
  • Resend: resent copy attributed to the acting admin.
  • Show page: the From row names the sender when set, "AWBW Portal" when not (scoped to the row's <dd>).
  • Bulk invite: sender_id threaded to the job, and the job passes it into send_confirmation_instructions.
  • Decorator: sender_name for both branches.

Notes

  • Deliberately not using Current.user: a background job shouldn't mutate request-scoped state, and Current.user is what gates AhoyTrackable#track_lifecycle_event — setting it in a job made every invite push an event onto LifecycleBuffer, a thread-local only ApplicationController flushes. Those piled up unflushed and undelivered.
  • The five call sites (bulk invite job, users#send_welcome_instructions, ProcessEmailChange, ProcessEmailManualConfirm, ProcessConfirmation) pass the sender explicitly; the last four sit right next to an existing updated_by = @current_user "credit the acting admin" line.
  • sender: on the console bulk invite is optional — omitting it falls back to "AWBW Portal" and logs a warning.
  • The existing auth.confirmation_email_sent ahoy event is attributed to the sender too, when there is one.

Copilot AI lite review requested due to automatic review settings August 4, 2026 05:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 13:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale maebeale changed the title Attribute invite emails to the sending person Attribute admin-sent emails to the sending person (invites, reminders, resends) + show From Aug 4, 2026
@maebeale
maebeale marked this pull request as ready for review August 4, 2026 13:35
maebeale and others added 2 commits August 4, 2026 09:35
Invitation emails (both the single Invite button and the bulk console
tool) were recorded with no sender, so the notifications UI showed them
as "From: AWBW Portal" — anonymous. Attribute them to a real person.

DeviseMailer now records Current.user as the notification sender. The
Invite button already runs in a request where Current.user is set, so it
gets this for free. The bulk path runs in the console with no request, so
BulkInviteService takes a sender: and threads it through the job, which
sets Current.user before sending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Event reminders sent by hand from the bulk reminders page, and resent
notifications, were created with no sender — so the communications index
and show page labeled them "AWBW Portal" as if the portal sent them
automatically. Pass the acting admin as the sender on both paths, and add
a From row to the notification show page (person's name when a staff
member sent it, "AWBW Portal" only for truly automated messages).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 4, 2026 13:36
@maebeale
maebeale force-pushed the maebeale/bulk-invite-sender-attribution branch from dd4f35c to 287f1d0 Compare August 4, 2026 13:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Threading the sender through the global Current.user meant a background job
was mutating request-scoped state to talk to the mailer. It also silently
un-gated AhoyTrackable's lifecycle tracking (which keys off Current.user):
send_confirmation_instructions saves the record, so every invite pushed an
event onto LifecycleBuffer, a thread-local that only ApplicationController
ever flushes — in a job those piled up unflushed and undelivered.

Devise sends with deliver_now, so the mailer holds the same User instance the
caller does; the sender can just ride along on the record. The four
admin-initiated call sites that were relying on ApplicationController setting
Current.user now pass current_user explicitly, so they keep their attribution.

Also collapses the "sender name, else AWBW Portal" fallback into
NotificationDecorator#sender_name — the index was rendering a lowercase
"AWBW portal" while the row partial and detail page said "AWBW Portal".

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 4, 2026 14:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread app/models/user.rb
# The staff member who triggered this confirmation email, when one did. Not
# persisted — DeviseMailer reads it off the record to attribute the
# notification it logs, since it has no request and no current_user.
attr_reader :confirmation_sender

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: This relies on Devise's default send_devise_notification using deliver_now, so DeviseMailer receives the same in-memory User the caller set the sender on. If anyone ever overrides it to deliver_later, the record round-trips through GlobalID and this ivar is silently lost — the notification would go back to reading "AWBW Portal".

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.

2 participants