OUT-3925, OUT-3926 | Extend grouped email system to support IU recipients - #1362
Conversation
…ents - Wire recipientIuId into bufferGroupedEmailEvent with a separate window query and IU-specific window key (avoids collisions with CU keys) - Teach flushGroupedEmail to group and flush IU recipient rows via recipientInternalUserId in the Copilot notification payload - Add isIuEmailEnabled() stub backed by IU_EMAIL_ALWAYS_ENABLED env var so IU emails can be tested before OUT-3929 (platform preference flag) ships - Remove disableEmail hard-block for IU assignees; gate on isIuEmailEnabled() - Fix buildNotificationDetails to accept explicit isIuRecipient flag so the payload sets recipientInternalUserId correctly when IU has email Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR extends the grouped email buffering system to support Internal User (IU) recipients, gated behind an
Confidence Score: 5/5Safe to merge; the IU email path is entirely behind the IU_EMAIL_ALWAYS_ENABLED flag so prod behaviour is unchanged until explicitly opted in. The IU flush loop faithfully mirrors the existing CU loop, the window-key namespace prevents collisions, the feature flag correctly gates all buffering through disableEmail, and the three new guard tests cover the key wiring boundaries. The only open item is a type-safety gap in SendGroupedEmailArgs that does not affect any current call site. src/jobs/notifications/send-grouped-email.ts — the optional recipient fields have no compile-time enforcement that at least one is provided. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant TN as TaskNotificationsService
participant NS as NotificationService
participant DB as GroupedEmailEvents (DB)
participant TQ as Trigger.dev Queue
participant FG as flushGroupedEmail (job)
participant CP as Copilot API
TN->>NS: "create(Assigned/ReassignedToIU, task, {disableEmail: false})"
NS->>NS: "isIuRecipient = true"
NS->>NS: "groupedType = ASSIGNED"
NS->>DB: bufferGroupedEmailEvent (recipientIuId, windowKey: iuId:iu:uuid)
NS->>TQ: enqueueGroupedEmailFlush (delay 5m)
NS->>CP: createNotification (inProduct only)
Note over TQ,FG: 5 minutes later
TQ->>FG: "flushGroupedEmailRun({workspaceId, windowKey})"
FG->>DB: readUnsentWindowEvents
FG->>FG: groupIuRecipients(rows)
alt single live event
FG->>CP: sendIndividualEmail (recipientInternalUserId)
else multiple live events
FG->>CP: sendGroupedEmail (recipientInternalUserId)
end
FG->>DB: markIuRecipientSent
FG->>DB: deleteWindowRows (sentAt set)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant TN as TaskNotificationsService
participant NS as NotificationService
participant DB as GroupedEmailEvents (DB)
participant TQ as Trigger.dev Queue
participant FG as flushGroupedEmail (job)
participant CP as Copilot API
TN->>NS: "create(Assigned/ReassignedToIU, task, {disableEmail: false})"
NS->>NS: "isIuRecipient = true"
NS->>NS: "groupedType = ASSIGNED"
NS->>DB: bufferGroupedEmailEvent (recipientIuId, windowKey: iuId:iu:uuid)
NS->>TQ: enqueueGroupedEmailFlush (delay 5m)
NS->>CP: createNotification (inProduct only)
Note over TQ,FG: 5 minutes later
TQ->>FG: "flushGroupedEmailRun({workspaceId, windowKey})"
FG->>DB: readUnsentWindowEvents
FG->>FG: groupIuRecipients(rows)
alt single live event
FG->>CP: sendIndividualEmail (recipientInternalUserId)
else multiple live events
FG->>CP: sendGroupedEmail (recipientInternalUserId)
end
FG->>DB: markIuRecipientSent
FG->>DB: deleteWindowRows (sentAt set)
Reviews (2): Last reviewed commit: "fix(notifications): buffer ReassignedToI..." | Re-trigger Greptile |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Deployment failed with the following error: Learn More: https://vercel.link/multiple-function-regions |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
please review again @greptile |
…ipients Address PR review: pass recipient ids via conditional spread instead of explicit null, and drop the null type from bufferGroupedEmailEvent params. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rbitrary IU The flush job attributed grouped emails to resolveSenderId (an arbitrary workspace internal user). Copilot creates the notification but does not deliver the email to an IU recipient from an unrelated sender, so IU grouped emails (event count > 1) silently failed while single-event and client grouped emails worked. Use the sender captured on the buffered individualEmail and fall back to resolveSenderId only for pre-migration rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getEmailDetails had no ReassignedToIU entry, so reassignment to an IU produced no email and was never buffered into the grouped queue — only the in-product notification fired. Add the template (same copy as ReassignedToClient) and a regression test asserting IU-recipient actions have email templates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- rename isIuRecipient to isAssignedToIu / isRecipientIu for clarity - bufferGroupedEmailEvent now takes recipientId + companyId + isRecipientIu and routes to the right column internally - collapse the duplicated window queries into one with a recipient filter fragment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
recipientIuIdintobufferGroupedEmailEventwith a separate window query and IU-specific window key format ({iuId}:iu:{uuid}) to avoid collisions with CU keysflushGroupedEmailto group and flush IU buffer rows, sending torecipientInternalUserIdin the Copilot notification payloadisIuEmailEnabled()stub backed byIU_EMAIL_ALWAYS_ENABLEDenv var so IU emails can be tested locally before OUT-3929 (platform preference flag) shipsdisableEmailblock for IU assignees insendUserTaskNotification, gating it onisIuEmailEnabled()insteadbuildNotificationDetailsto accept an explicitisIuRecipientflag so the payload correctly setsrecipientInternalUserId(the old inference from absence-of-email breaks once IUs have email)Test plan
IU_EMAIL_ALWAYS_ENABLED=truein.env.localGroupedEmailEventsrow is written withrecipientIuIdset andrecipientClientIdnullflushGroupedEmailmanually) — confirm the email is sent to the IU viarecipientInternalUserIdIU_EMAIL_ALWAYS_ENABLEDunset — confirm no IU emails are sent (prod-safe)🤖 Generated with Claude Code