Skip to content

OUT-4000 | Filter out email notifications according to iu notification preference - #1395

Merged
arpandhakal merged 3 commits into
mainfrom
OUT-4000-filter-iu-email-by-preference
Jul 23, 2026
Merged

OUT-4000 | Filter out email notifications according to iu notification preference#1395
arpandhakal merged 3 commits into
mainfrom
OUT-4000-filter-iu-email-by-preference

Conversation

@arpandhakal

Copy link
Copy Markdown
Collaborator

Context

IU emails currently send unconditionally — the app ignores each IU's notification preferences. OUT-3929 built the gating infrastructure but left it disabled, because the platform had no way to read an IU's per-category preference. That endpoint now exists:

GET /v1/internal-users/:id/notification-settings

Only our three categories carry our appId + a notificationSettingId (same ids the app already resolves via resolveIuNotificationSettingId): newCommentOnATask → COMMENT, newTaskAssigned → ASSIGNED, taskCompleted → COMPLETED. We match by id, so we don't depend on the platform's internal category key names.

Approach — hybrid, split by send path

IU emails always divert into the 5-minute grouped buffer; in-product fires immediately. Gating is email-only (in-product unaffected).

  • Single emails (immediate sends + single-event grouped windows replayed verbatim): re-enable attaching notificationSettingId on the notification body and let the platform gate the send.
  • Grouped summary emails (multi-event windows): filterEventsForIuPreferences reads the recipient IU's prefs and drops events for categories whose email is disabled before composing the summary. Closes the mixed-category-window gap a single id can't cover (an all-disabled IU sends nothing). Fails open on read error.

Changes

  • src/utils/CopilotAPI.tsgetInternalUserNotificationSettings(id) via the existing _manualFetch + wrapWithRetry pattern (not cached — live preference state).
  • src/types/common.tsInternalUserNotificationSettingsSchema (notifyAbout as a z.record, robust to platform categories we don't own).
  • src/app/api/notification/iuEmailPreference.ts (new) — pure helpers disabledEmailSettingIds (matches by notificationSettingId) and isIuEmailGloballyOff.
  • src/app/api/notification/notification.service.ts — re-enable resolved notificationSettingId in create() + createBulkNotification().
  • src/jobs/notifications/flush-grouped-email.ts — implement filterEventsForIuPreferences.

Tests

  • New iuEmailPreference.test.ts; extended flush tests (per-category drop, all-disabled skip, global-off, fail-open); flipped the obsolete "ship ungated" guard in notification.service.test.ts.
  • yarn tsc ✓, affected suites green, prettier clean, lint 0 errors.

⚠️ Verify before merge

isIuEmailGloballyOff assumes emailSettings === 'not_active' means "no IU emails at all" and suppresses all. Confirm the exact emailSettings enum on staging before trusting the global gate — fall back to per-category disableEmail only if it behaves differently.

🤖 Generated with Claude Code

…n preference

Complete OUT-3929 IU email gating now that Copilot exposes
GET /v1/internal-users/:id/notification-settings.

Hybrid gating, split by send path:
- Single emails: attach the resolved notificationSettingId on the body
  so the platform gates the send (re-enabled in NotificationService).
- Grouped summaries: read the recipient IU's prefs and drop events for
  categories whose email is disabled before composing the summary
  (filterEventsForIuPreferences), fail-open on read error.

- add CopilotAPI.getInternalUserNotificationSettings (not cached; live pref)
- add InternalUserNotificationSettingsSchema
- add iuEmailPreference helpers (disabledEmailSettingIds, isIuEmailGloballyOff)
- tests for the helpers and the flush filter; flip obsolete ungated guard

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 21, 2026

Copy link
Copy Markdown

OUT-4000

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tasks-app Ready Ready Preview, Comment Jul 21, 2026 12:35pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR applies IU notification preferences to email delivery. The main changes are:

  • Adds a Copilot API call for IU notification settings.
  • Adds helpers for global and per-category email preference checks.
  • Attaches IU notification setting IDs during notification creation.
  • Filters grouped IU email events before composing summaries.
  • Adds tests for per-category drops, global opt-out, and fail-open behavior.

Confidence Score: 4/5

This is close, but the global email gate should be fixed before merging.

  • The grouped-email path can suppress all email when emailSettings is absent.
  • The schema currently accepts that absent field, so this can happen without triggering the fail-open error path.
  • The rest of the changed preference filtering follows the intended email-only flow.

src/app/api/notification/iuEmailPreference.ts, src/types/common.ts

Important Files Changed

Filename Overview
src/app/api/notification/iuEmailPreference.ts Adds preference helpers, but the global email check treats an accepted missing field as globally off.
src/jobs/notifications/flush-grouped-email.ts Adds grouped IU email filtering using live preferences and preserves fail-open behavior on read errors.
src/app/api/notification/notification.service.ts Re-enables resolved IU notification setting IDs on notification creation paths.
src/types/common.ts Adds the IU notification settings schema with an optional global email state.
src/utils/CopilotAPI.ts Adds the internal-user notification-settings API wrapper with retry support.

Reviews (2): Last reviewed commit: "OUT-4000 | Fail closed on the global IU ..." | Re-trigger Greptile

Comment thread src/app/api/notification/iuEmailPreference.ts Outdated
Only treat email as globally enabled when emailSettings is explicitly
"active" (case-insensitive, trimmed). Any other value — a different
disabled string, unexpected casing, or a missing field — is treated as
not activated, so a global opt-out is never bypassed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Deployment failed with the following error:

Deploying Serverless Functions to multiple regions is restricted to the Pro and Enterprise plans.

Learn More: https://vercel.link/multiple-function-regions

@arpandhakal

Copy link
Copy Markdown
Collaborator Author

please re review @greptile

// email notifications. Any other value — a different disabled string, unexpected casing, or a
// missing field — is treated as not activated, so a global opt-out is never bypassed.
export const isIuEmailGloballyOff = (settings: InternalUserNotificationSettings): boolean =>
(settings.emailSettings ?? '').trim().toLowerCase() !== 'active'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing State Suppresses Email

This gate treats a missing emailSettings field as globally off, but the parsed settings schema accepts that field as optional. If the platform returns a valid payload with only per-category notifyAbout data, or an existing IU has not had this global field backfilled, grouped email filtering returns no events before checking the category settings. The IU then receives no grouped email even when their category email settings are enabled. The global gate should rely on the confirmed enum/default, or only suppress on confirmed disabled values.

Knowledge Base Used: Notifications flow: from task/comment event to delivered notification

…isableEmail

emailSettings "not_active" does NOT mean the IU receives no email — a test
IU with emailSettings=not_active still expects (and should get) comment
emails. Gating on it suppressed every grouped IU email for that IU. The
reliable signal is the per-category disableEmail flag, so gate on that alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm

@arpandhakal
arpandhakal merged commit 9f7fe84 into main Jul 23, 2026
3 checks passed
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