Skip to content

feat: move notifications out of activity log space #35233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 18, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ee/session_recordings/session_summary/summarize_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)
from ee.session_recordings.session_summary.prompt_data import SessionSummaryPromptData
from ee.session_recordings.session_summary.utils import load_custom_template, shorten_url
from posthog.api.activity_log import ServerTimingsGathered
from posthog.api.utils import ServerTimingsGathered
from posthog.session_recordings.models.metadata import RecordingMetadata
from posthog.sync import database_sync_to_async

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ActivityScope, AvailableFeature } from '~/types'
import { SidePanelPaneHeader } from '../../components/SidePanelPaneHeader'
import { SidePanelActivityMetalytics } from './SidePanelActivityMetalytics'
import { SidePanelActivitySubscriptions } from './SidePanelActivitySubscriptions'
import { sidePanelNotificationsLogic } from '~/layout/navigation-3000/sidepanel/panels/activity/sidePanelNotificationsLogic'

const SCROLL_TRIGGER_OFFSET = 100

Expand All @@ -36,20 +37,14 @@ export const SidePanelActivityIcon = (props: { className?: string }): JSX.Elemen
}

export const SidePanelActivity = (): JSX.Element => {
const {
hasNotifications,
notifications,
activeTab,
allActivity,
allActivityResponseLoading,
allActivityHasNext,
importantChangesLoading,
hasUnread,
filters,
filtersForCurrentPage,
} = useValues(sidePanelActivityLogic)
const { togglePolling, setActiveTab, maybeLoadOlderActivity, markAllAsRead, loadImportantChanges, setFilters } =
useActions(sidePanelActivityLogic)
const { activeTab, allActivity, allActivityResponseLoading, allActivityHasNext, filters, filtersForCurrentPage } =
useValues(sidePanelActivityLogic)
const { setActiveTab, maybeLoadOlderActivity, setFilters } = useActions(sidePanelActivityLogic)

const { hasNotifications, notifications, importantChangesLoading, hasUnread } =
useValues(sidePanelNotificationsLogic)
const { togglePolling, markAllAsRead, loadImportantChanges } = useActions(sidePanelNotificationsLogic)

const { user } = useValues(userLogic)
const { featureFlags } = useValues(featureFlagLogic)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { actions, afterMount, beforeUnmount, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { lazyLoaders } from 'kea-loaders'
import { subscriptions } from 'kea-subscriptions'
import api, { PaginatedResponse } from 'lib/api'
import { describerFor } from 'lib/components/ActivityLog/activityLogLogic'
import { ActivityLogItem, humanize, HumanizedActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity'
import { dayjs } from 'lib/dayjs'
import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown'
import { toParams } from 'lib/utils'
import posthog, { JsonRecord } from 'posthog-js'
import { projectLogic } from 'scenes/projectLogic'

import { ActivityScope, UserBasicType } from '~/types'
Expand All @@ -17,30 +13,12 @@ import { SidePanelSceneContext } from '../../types'
import { sidePanelContextLogic } from '../sidePanelContextLogic'
import type { sidePanelActivityLogicType } from './sidePanelActivityLogicType'

const POLL_TIMEOUT = 5 * 60 * 1000

export type ActivityFilters = {
scope?: ActivityScope | string
item_id?: ActivityLogItem['item_id']
user?: UserBasicType['id']
}

export interface ChangelogFlagPayload {
notificationDate: dayjs.Dayjs

// Images can be embedded directly in the markdown using ![alt text](url) syntax.
// LemonMarkdown will render them.
// For optimal display, ensure images are reasonably sized (e.g., width < 800px)
// and optimized for web (e.g., < 500KB).
// We suggest you upload it to a CDN to reduce load times/server load.
// If you're a PostHog employee, check https://posthog.com/handbook/engineering/posthog-com/assets out
markdown: string

// Optional fields used if you wanna override this to a specific person rather than Joe
name?: string
email?: string
}

export interface ChangesResponse {
results: ActivityLogItem[]
next: string | null
Expand All @@ -61,15 +39,10 @@ export const sidePanelActivityLogic = kea<sidePanelActivityLogicType>([
actions: [sidePanelStateLogic, ['openSidePanel']],
})),
actions({
togglePolling: (pageIsVisible: boolean) => ({ pageIsVisible }),
incrementErrorCount: true,
clearErrorCount: true,
markAllAsRead: true,
setActiveTab: (tab: SidePanelActivityTab) => ({ tab }),
loadAllActivity: true,
loadOlderActivity: true,
maybeLoadOlderActivity: true,
loadImportantChanges: (onlyUnread = true) => ({ onlyUnread }),
setFilters: (filters: ActivityFilters | null) => ({ filters }),
setFiltersForCurrentPage: (filters: ActivityFilters | null) => ({ filters }),
}),
Expand All @@ -81,13 +54,6 @@ export const sidePanelActivityLogic = kea<sidePanelActivityLogicType>([
setActiveTab: (_, { tab }) => tab,
},
],
errorCounter: [
0,
{
incrementErrorCount: (state) => (state >= 5 ? 5 : state + 1),
clearErrorCount: () => 0,
},
],
filters: [
null as ActivityFilters | null,
{
Expand All @@ -102,68 +68,7 @@ export const sidePanelActivityLogic = kea<sidePanelActivityLogicType>([
},
],
}),
lazyLoaders(({ actions, values, cache }) => ({
importantChanges: [
null as ChangesResponse | null,
{
loadImportantChanges: async ({ onlyUnread }, breakpoint) => {
await breakpoint(1)

clearTimeout(cache.pollTimeout)

try {
const response = await api.get<ChangesResponse>(
`api/projects/${values.currentProjectId}/activity_log/important_changes?` +
toParams({ unread: onlyUnread })
)

// we can't rely on automatic success action here because we swallow errors so always succeed
actions.clearErrorCount()
return response
} catch {
// swallow errors as this isn't user initiated
// increment a counter to backoff calling the API while errors persist
actions.incrementErrorCount()
return null
} finally {
const pollTimeoutMilliseconds = values.errorCounter
? POLL_TIMEOUT * values.errorCounter
: POLL_TIMEOUT

cache.pollTimeout = window.setTimeout(actions.loadImportantChanges, pollTimeoutMilliseconds)
}
},
markAllAsRead: async () => {
const current = values.importantChanges
if (!current) {
return null
}

const latestNotification = values.notifications.reduce((a, b) =>
a.created_at.isAfter(b.created_at) ? a : b
)

const hasUnread = values.notifications.some((ic) => ic.unread)

if (!hasUnread) {
return current
}

await api.create(
`api/projects/${values.currentProjectId}/activity_log/bookmark_activity_notification`,
{
bookmark: latestNotification.created_at.toISOString(),
}
)

return {
last_read: latestNotification.created_at.toISOString(),
next: current.next,
results: current.results.map((ic) => ({ ...ic, unread: false })),
}
},
},
],
lazyLoaders(({ values }) => ({
allActivityResponse: [
null as PaginatedResponse<ActivityLogItem> | null,
{
Expand All @@ -173,7 +78,6 @@ export const sidePanelActivityLogic = kea<sidePanelActivityLogicType>([
breakpoint()
return response
},

loadOlderActivity: async (_, breakpoint) => {
await breakpoint(1)

Expand All @@ -197,7 +101,6 @@ export const sidePanelActivityLogic = kea<sidePanelActivityLogicType>([
actions.loadAllActivity()
}
},

maybeLoadOlderActivity: () => {
if (!values.allActivityResponseLoading && values.allActivityResponse?.next) {
actions.loadOlderActivity()
Expand All @@ -224,71 +127,6 @@ export const sidePanelActivityLogic = kea<sidePanelActivityLogicType>([
},
],
allActivityHasNext: [(s) => [s.allActivityResponse], (allActivityResponse) => !!allActivityResponse?.next],
notifications: [
(s) => [s.importantChanges],
(importantChanges): HumanizedActivityLogItem[] => {
try {
let importantChangesHumanized = humanize(importantChanges?.results || [], describerFor, true)

const flagPayload = posthog.getFeatureFlagPayload('changelog-notification')
const changelogNotifications = flagPayload
? (flagPayload as JsonRecord[]).map(
(notification) =>
({
markdown: notification.markdown,
notificationDate: dayjs(notification.notificationDate as string),
email: notification.email,
name: notification.name,
} as ChangelogFlagPayload)
)
: null

if (changelogNotifications) {
const lastRead = importantChanges?.last_read ? dayjs(importantChanges.last_read) : null

importantChangesHumanized = [
...importantChangesHumanized,
...changelogNotifications.map(
(changelogNotification) =>
({
email: changelogNotification.email || '[email protected]',
name: changelogNotification.name || 'Joe',
isSystem: true,
description: <LemonMarkdown>{changelogNotification.markdown}</LemonMarkdown>,
created_at: changelogNotification.notificationDate,
unread: lastRead?.isSameOrBefore(changelogNotification.notificationDate),
} as HumanizedActivityLogItem)
),
]

// Sorting this inside the `if` case because there's no need to sort the changelog notifications
// if there are no changelog notifications, since they come from the backend sorted already.
importantChangesHumanized.sort((a: HumanizedActivityLogItem, b: HumanizedActivityLogItem) => {
if (a.created_at.isBefore(b.created_at)) {
return 1
} else if (a.created_at.isAfter(b.created_at)) {
return -1
}

return 0
})
}

return importantChangesHumanized
} catch {
// swallow errors as this isn't user initiated
return []
}
},
],

hasNotifications: [(s) => [s.notifications], (notifications) => !!notifications.length],
unread: [
(s) => [s.notifications],
(notifications: HumanizedActivityLogItem[]) => notifications.filter((ic) => ic.unread),
],
unreadCount: [(s) => [s.unread], (unread) => (unread || []).length],
hasUnread: [(s) => [s.unreadCount], (unreadCount) => unreadCount > 0],
}),

subscriptions(({ actions, values }) => ({
Expand All @@ -314,8 +152,4 @@ export const sidePanelActivityLogic = kea<sidePanelActivityLogicType>([
const activityFilters = values.sceneSidePanelContext
actions.setFiltersForCurrentPage(activityFilters ? { ...values.filters, ...activityFilters } : null)
}),

beforeUnmount(({ cache }) => {
clearTimeout(cache.pollTimeout)
}),
])
Loading
Loading