diff --git a/static/app/views/issueDetails/activitySection/activityLineItem/activityFeedItem.spec.ts b/static/app/views/issueDetails/activitySection/activityLineItem/activityFeedItem.spec.ts new file mode 100644 index 000000000000..e5e5f0421e98 --- /dev/null +++ b/static/app/views/issueDetails/activitySection/activityLineItem/activityFeedItem.spec.ts @@ -0,0 +1,178 @@ +import {ActivityFeedFixture} from 'sentry-fixture/activityFeed'; +import {UserFixture} from 'sentry-fixture/user'; + +import {GroupActivityType, PriorityLevel} from 'sentry/types/group'; + +import { + type ActivityFeedItem, + collapseFlappingStatusActivities, + type DisplayedActivityFeedItem, +} from './activityFeedItem'; + +type ActivityFixtureParams = NonNullable[0]>; + +function activity(params: ActivityFixtureParams): ActivityFeedItem { + return { + type: 'activity', + activity: {...ActivityFeedFixture(params), user: params.user ?? null}, + }; +} + +function regression(): ActivityFeedItem { + return activity({type: GroupActivityType.SET_REGRESSION, data: {}}); +} + +function resolved(): ActivityFeedItem { + return activity({type: GroupActivityType.SET_RESOLVED, data: {}}); +} + +function ongoing(): ActivityFeedItem { + return activity({ + type: GroupActivityType.AUTO_SET_ONGOING, + data: {after_days: 7}, + }); +} + +function resolvedByAge(): ActivityFeedItem { + return activity({type: GroupActivityType.SET_RESOLVED_BY_AGE, data: {age: 7}}); +} + +function expectCollapsedActivities( + item: DisplayedActivityFeedItem | undefined, + expectedActivities: ActivityFeedItem[] +) { + expect(item).toMatchObject({ + type: 'collapsed_status_activities', + activities: expectedActivities, + }); +} + +describe('collapseFlappingStatusActivities', () => { + it('keeps the newest pair visible and merges adjacent older pairs into one rollup', () => { + const newestRegression = regression(); + const newestResolution = resolved(); + const olderRun = [ + activity({ + type: GroupActivityType.SET_PRIORITY, + data: {priority: PriorityLevel.HIGH, reason: 'issue_platform'}, + }), + regression(), + resolved(), + ongoing(), + regression(), + resolved(), + ]; + const result = collapseFlappingStatusActivities([ + newestRegression, + newestResolution, + ...olderRun, + ]); + + expect(result.slice(0, 2)).toEqual([newestRegression, newestResolution]); + expect(result).toHaveLength(3); + expectCollapsedActivities(result[2], olderRun); + }); + + it('keeps the newest pair visible when newer user activity exists', () => { + const manualReopen = activity({ + type: GroupActivityType.SET_UNRESOLVED, + data: {}, + user: UserFixture(), + }); + const newestRun = [ongoing(), regression(), resolvedByAge()]; + const olderRun = [ongoing(), regression(), resolvedByAge()]; + const result = collapseFlappingStatusActivities([ + manualReopen, + ...newestRun, + ...olderRun, + ]); + + expect(result.slice(0, 4)).toEqual([manualReopen, ...newestRun]); + expect(result).toHaveLength(5); + expectCollapsedActivities(result[4], olderRun); + }); + + it('recognizes a resolved status transition', () => { + const newestPair = [regression(), resolved()]; + const flappingPair = [ + regression(), + activity({ + type: GroupActivityType.SET_RESOLVED_IN_RELEASE, + data: {version: '1.0.0'}, + }), + ]; + const result = collapseFlappingStatusActivities([...newestPair, ...flappingPair]); + + expectCollapsedActivities(result[2], flappingPair); + }); + + it('absorbs automatic lifecycle and priority activity into a flapping run', () => { + const automaticRun = [ + resolved(), + activity({type: GroupActivityType.SET_UNRESOLVED, data: {}}), + activity({ + type: GroupActivityType.SET_PRIORITY, + data: {priority: PriorityLevel.HIGH, reason: 'issue_platform'}, + }), + ongoing(), + activity({ + type: GroupActivityType.SET_PRIORITY, + data: {priority: PriorityLevel.MEDIUM, reason: 'ongoing'}, + }), + activity({type: GroupActivityType.SET_ESCALATING, data: {}}), + activity({ + type: GroupActivityType.SET_PRIORITY, + data: {priority: PriorityLevel.HIGH, reason: 'escalating'}, + }), + regression(), + ]; + const result = collapseFlappingStatusActivities([ + activity({ + type: GroupActivityType.SET_UNRESOLVED, + data: {}, + user: UserFixture(), + }), + ...automaticRun, + ]); + + expectCollapsedActivities(result[1], automaticRun); + }); + + it('does not hide an incomplete automatic run', () => { + const activities = [ + resolved(), + ongoing(), + activity({ + type: GroupActivityType.SET_PRIORITY, + data: {priority: PriorityLevel.MEDIUM, reason: 'ongoing'}, + }), + ]; + + expect(collapseFlappingStatusActivities(activities)).toEqual(activities); + }); + + it('keeps other priority activity as a boundary before an older rollup', () => { + const manualReopen = activity({ + type: GroupActivityType.SET_UNRESOLVED, + data: {}, + user: UserFixture(), + }); + const otherPriority = activity({ + type: GroupActivityType.SET_PRIORITY, + data: {priority: PriorityLevel.LOW, reason: 'other'}, + }); + const newerRun = [regression(), resolved()]; + const olderRun = [regression(), resolved()]; + const result = collapseFlappingStatusActivities([ + manualReopen, + ...newerRun, + otherPriority, + ...olderRun, + ]); + + expect(result.slice(0, 3)).toEqual([manualReopen, ...newerRun]); + expect(result).toHaveLength(5); + expect(result[3]).toBe(otherPriority); + expectCollapsedActivities(result[4], olderRun); + }); +}); diff --git a/static/app/views/issueDetails/activitySection/activityLineItem/activityFeedItem.ts b/static/app/views/issueDetails/activitySection/activityLineItem/activityFeedItem.ts index 3c537e5b8ead..5b7ceb769987 100644 --- a/static/app/views/issueDetails/activitySection/activityLineItem/activityFeedItem.ts +++ b/static/app/views/issueDetails/activitySection/activityLineItem/activityFeedItem.ts @@ -1,5 +1,6 @@ import type {GroupActivity} from 'sentry/types/group'; import {GroupActivityType, SEER_ACTIVITY_TYPES} from 'sentry/types/group'; +import {deduplicatePullRequestActivities} from 'sentry/views/issueDetails/activitySection/activityLineItem/deduplicatePullRequestActivities'; type ActivityOfType = Extract< GroupActivity, @@ -33,6 +34,64 @@ export type ActivityFeedItem = } | CollapsedSeerActivity; +interface CollapsedStatusActivity { + activities: ActivityFeedItem[]; + /** The first rolled-up item provides a stable key for the summary row. */ + activity: GroupActivity; + type: 'collapsed_status_activities'; +} + +export type DisplayedActivityFeedItem = ActivityFeedItem | CollapsedStatusActivity; + +const RESOLUTION_ACTIVITY_TYPES = new Set([ + GroupActivityType.SET_RESOLVED, + GroupActivityType.SET_RESOLVED_BY_AGE, + GroupActivityType.SET_RESOLVED_IN_RELEASE, + GroupActivityType.SET_RESOLVED_IN_COMMIT, +]); +// SET_RESOLVED_IN_PULL_REQUEST is intentionally absent: despite its backend name, it records +// that an issue was referenced in a pull request rather than a resolved status transition. + +// These are the complete set of backend reasons for automatic priority changes. Priority +// activities without one of these reasons should remain visible boundaries. +const FLAPPING_PRIORITY_REASONS = new Set(['escalating', 'issue_platform', 'ongoing']); + +function isResolutionActivity(activity: ActivityFeedItem): boolean { + return RESOLUTION_ACTIVITY_TYPES.has(activity.activity.type); +} + +function isFlappingStatusActivity(activity: ActivityFeedItem): boolean { + if (isResolutionActivity(activity)) { + return true; + } + + const groupActivity = activity.activity; + switch (groupActivity.type) { + case GroupActivityType.SET_REGRESSION: + case GroupActivityType.AUTO_SET_ONGOING: + case GroupActivityType.SET_ESCALATING: + return true; + case GroupActivityType.SET_UNRESOLVED: + // A user-authored reopen is meaningful history and splits automatic flapping runs. + return !groupActivity.user; + case GroupActivityType.SET_PRIORITY: + return FLAPPING_PRIORITY_REASONS.has(groupActivity.data.reason); + default: + return false; + } +} + +/** + * A run is only noise once it contains both sides of a resolve/regress flap. This keeps isolated + * automatic lifecycle updates visible. + */ +function isCollapsibleStatusRun(run: ActivityFeedItem[]): boolean { + return ( + run.some(item => item.activity.type === GroupActivityType.SET_REGRESSION) && + run.some(isResolutionActivity) + ); +} + function getSeerRunId(activity: GroupActivity): number | undefined { if (!('run_id' in activity.data)) { return undefined; @@ -150,3 +209,100 @@ export function collapseSeerActivityPairs( return collapsedActivities; } + +/** + * - Keep the newest adjacent regression/resolution pair and everything newer visible. + * - Treat unrelated or user-authored activity as a boundary between runs. + * - Collapse each remaining consecutive run only when it contains both a resolution and a + * regression; keep incomplete runs visible. + * - Match loosely by activity type and feed order because these events have no shared identifier. + */ +export function collapseFlappingStatusActivities( + activities: ActivityFeedItem[] +): DisplayedActivityFeedItem[] { + const latestPairIndex = activities.findIndex((activity, index) => { + const nextActivity = activities[index + 1]; + return ( + activity.activity.type === GroupActivityType.SET_REGRESSION && + nextActivity !== undefined && + isResolutionActivity(nextActivity) + ); + }); + + // Preserve everything through the newest pair; +2 includes both the regression and resolution. + const protectedActivityCount = latestPairIndex === -1 ? 0 : latestPairIndex + 2; + const displayedActivities: DisplayedActivityFeedItem[] = activities.slice( + 0, + protectedActivityCount + ); + + for (let index = protectedActivityCount; index < activities.length;) { + const activity = activities[index]; + if (!activity) { + break; + } + + if (!isFlappingStatusActivity(activity)) { + displayedActivities.push(activity); + index += 1; + continue; + } + + const run: [ActivityFeedItem, ...ActivityFeedItem[]] = [activity]; + index += 1; + while (index < activities.length) { + const runActivity = activities[index]; + if (!runActivity || !isFlappingStatusActivity(runActivity)) { + break; + } + run.push(runActivity); + index += 1; + } + + if (isCollapsibleStatusRun(run)) { + displayedActivities.push({ + type: 'collapsed_status_activities', + activity: run[0].activity, + activities: run, + }); + } else { + displayedActivities.push(...run); + } + } + + return displayedActivities; +} + +interface BuildActivityFeedItemsOptions { + activities: GroupActivity[]; + showSeerActivities: boolean; + showStatusFlappingRollups: boolean; + filterComments?: boolean; +} + +export function buildActivityFeedItems({ + activities, + filterComments, + showSeerActivities, + showStatusFlappingRollups, +}: BuildActivityFeedItemsOptions): DisplayedActivityFeedItem[] { + // Apply Seer visibility first so a hidden Seer PR activity cannot be attached as the actor + // for an otherwise visible pull request activity during deduplication. + const visibleActivities = showSeerActivities + ? activities + : activities.filter(item => !SEER_ACTIVITY_TYPES.has(item.type)); + const {activities: deduplicatedActivities, actorActivityById} = + deduplicatePullRequestActivities(visibleActivities); + const filteredActivities = deduplicatedActivities.filter( + item => !filterComments || item.type === GroupActivityType.NOTE + ); + const activityFeedItems = collapseSeerActivityPairs(filteredActivities).map(item => { + const actorActivity = actorActivityById.get(item.activity.id); + return item.type === 'activity' && actorActivity ? {...item, actorActivity} : item; + }); + + // Collapse status flapping last so expanding a rollup restores the fully processed feed items. + return showStatusFlappingRollups + ? collapseFlappingStatusActivities(activityFeedItems) + : activityFeedItems; +} diff --git a/static/app/views/issueDetails/activitySection/activityLineItem/collapsedStatusActivityRow.tsx b/static/app/views/issueDetails/activitySection/activityLineItem/collapsedStatusActivityRow.tsx new file mode 100644 index 000000000000..6733fd23a510 --- /dev/null +++ b/static/app/views/issueDetails/activitySection/activityLineItem/collapsedStatusActivityRow.tsx @@ -0,0 +1,72 @@ +import {Fragment, type ReactNode, useState} from 'react'; + +import {Button} from '@sentry/scraps/button'; +import {Flex} from '@sentry/scraps/layout'; +import {Text} from '@sentry/scraps/text'; + +import {t, tn} from 'sentry/locale'; +import {ActivityLineRow} from 'sentry/views/issueDetails/activitySection/activityLineItem/layout'; +import {ActivityLineDotMarker} from 'sentry/views/issueDetails/activitySection/activityLineItem/progressMarker'; + +interface CollapsedStatusActivityRowProps { + children: ReactNode; + eventCount: number; +} + +interface StatusActivityToggleRowProps { + label: ReactNode; + onClick: () => void; +} + +export function CollapsedStatusActivityRow({ + children, + eventCount, +}: CollapsedStatusActivityRowProps) { + const [isExpanded, setIsExpanded] = useState(false); + + if (!isExpanded) { + return ( + setIsExpanded(true)} + /> + ); + } + + return ( + + {children} + setIsExpanded(false)} + /> + + ); +} + +function StatusActivityToggleRow({label, onClick}: StatusActivityToggleRowProps) { + return ( + + + + + + + ); +} diff --git a/static/app/views/issueDetails/activitySection/activityLineItem/deduplicatePullRequestActivities.ts b/static/app/views/issueDetails/activitySection/activityLineItem/deduplicatePullRequestActivities.ts new file mode 100644 index 000000000000..36f0d0cc6a9a --- /dev/null +++ b/static/app/views/issueDetails/activitySection/activityLineItem/deduplicatePullRequestActivities.ts @@ -0,0 +1,69 @@ +import type {GroupActivity} from 'sentry/types/group'; +import {GroupActivityType} from 'sentry/types/group'; + +function isDuplicatePullRequestActivity( + activity: GroupActivity, + adjacentActivity: GroupActivity | undefined +): boolean { + switch (activity.type) { + case GroupActivityType.REFERENCED_IN_COMMIT: { + if (adjacentActivity?.type !== GroupActivityType.PULL_REQUEST_MERGED) { + return false; + } + + const pullRequest = activity.data.commit?.pullRequest; + const adjacentPullRequest = adjacentActivity.data.pullRequest; + if (!pullRequest || !adjacentPullRequest) { + return false; + } + + return ( + pullRequest.id === adjacentPullRequest.id && + pullRequest.repository.id === adjacentPullRequest.repository.id + ); + } + case GroupActivityType.SEER_PR_CREATED: { + if (adjacentActivity?.type !== GroupActivityType.SET_RESOLVED_IN_PULL_REQUEST) { + return false; + } + + const adjacentPullRequest = adjacentActivity.data.pullRequest; + if (!adjacentPullRequest) { + return false; + } + + return Boolean( + activity.data.pull_requests?.some( + pullRequest => + pullRequest.pull_request.pr_url === adjacentPullRequest.externalUrl + ) + ); + } + default: + return false; + } +} + +/** + * Removes redundant pull request activity while preserving a removed Seer event as the actor + * for the pull request activity that remains. + */ +export function deduplicatePullRequestActivities(activities: GroupActivity[]): { + activities: GroupActivity[]; + actorActivityById: Map; +} { + const actorActivityById = new Map(); + const filteredActivities = activities.filter((activity, index) => { + const duplicateActivity = [activities[index - 1], activities[index + 1]].find( + adjacentActivity => isDuplicatePullRequestActivity(activity, adjacentActivity) + ); + + if (activity.type === GroupActivityType.SEER_PR_CREATED && duplicateActivity) { + actorActivityById.set(duplicateActivity.id, activity); + } + + return !duplicateActivity; + }); + + return {activities: filteredActivities, actorActivityById}; +} diff --git a/static/app/views/issueDetails/activitySection/activityLineItem/progressMarker/index.tsx b/static/app/views/issueDetails/activitySection/activityLineItem/progressMarker/index.tsx index ccbcf43f17ad..576453441b32 100644 --- a/static/app/views/issueDetails/activitySection/activityLineItem/progressMarker/index.tsx +++ b/static/app/views/issueDetails/activitySection/activityLineItem/progressMarker/index.tsx @@ -35,6 +35,16 @@ export function ActivityLineMarker({ ); } +export function ActivityLineDotMarker() { + return ( + + + + + + ); +} + function ActivityLineDot() { return ; } diff --git a/static/app/views/issueDetails/activitySection/index.spec.tsx b/static/app/views/issueDetails/activitySection/index.spec.tsx index 9cbd92f8819f..85b5f8b3bce8 100644 --- a/static/app/views/issueDetails/activitySection/index.spec.tsx +++ b/static/app/views/issueDetails/activitySection/index.spec.tsx @@ -636,6 +636,89 @@ describe('ActivitySection', () => { expect(screen.getByText(/after 2 days of inactivity/)).toBeInTheDocument(); }); + const statusFlappingRollupFeature = 'issue-activity-status-flapping-rollup'; + + function makeFlappingGroup(id: string) { + return GroupFixture({ + id, + activity: [ + { + type: GroupActivityType.SET_REGRESSION, + id: `${id}-regressed-2`, + dateCreated: '2020-01-01T06:00:00Z', + data: {}, + }, + { + type: GroupActivityType.SET_RESOLVED, + id: `${id}-resolved-2`, + dateCreated: '2020-01-01T05:00:00Z', + data: {}, + }, + { + type: GroupActivityType.SET_REGRESSION, + id: `${id}-regressed-1`, + dateCreated: '2020-01-01T04:00:00Z', + data: {}, + }, + { + type: GroupActivityType.SET_RESOLVED, + id: `${id}-resolved-1`, + dateCreated: '2020-01-01T03:00:00Z', + data: {}, + }, + ], + project, + }); + } + + it('expands and collapses a status-flapping rollup when enabled', async () => { + const flappingGroup = makeFlappingGroup('1348'); + + render( + + + , + { + organization: OrganizationFixture({features: [statusFlappingRollupFeature]}), + } + ); + + expect(screen.getAllByText('Regressed')).toHaveLength(1); + expect(screen.getAllByText('Resolved')).toHaveLength(1); + expect(screen.getAllByRole('img', {name: 'Activity update'})).toHaveLength(3); + + await userEvent.click(screen.getByRole('button', {name: 'Show 2 more'})); + + expect(screen.queryByRole('button', {name: 'Show 2 more'})).not.toBeInTheDocument(); + expect(screen.getAllByText('Regressed')).toHaveLength(2); + expect(screen.getAllByText('Resolved')).toHaveLength(2); + + await userEvent.click(screen.getByRole('button', {name: 'Hide 2 events'})); + + expect(screen.getByRole('button', {name: 'Show 2 more'})).toBeInTheDocument(); + expect(screen.getAllByText('Regressed')).toHaveLength(1); + expect(screen.getAllByText('Resolved')).toHaveLength(1); + }); + + it('does not count rolled-up events as hidden sidebar rows', () => { + const flappingGroup = makeFlappingGroup('1350'); + + render( + + + , + { + organization: OrganizationFixture({features: [statusFlappingRollupFeature]}), + } + ); + + expect(screen.getByRole('button', {name: 'Show 2 more'})).toBeInTheDocument(); + expect(screen.getByText('Expand')).toBeInTheDocument(); + expect(screen.queryByText('View 2 more')).not.toBeInTheDocument(); + expect(screen.getAllByText('Regressed')).toHaveLength(1); + expect(screen.getAllByText('Resolved')).toHaveLength(1); + }); + it('renders note and allows for edit', async () => { jest.spyOn(indicators, 'addSuccessMessage'); diff --git a/static/app/views/issueDetails/activitySection/index.tsx b/static/app/views/issueDetails/activitySection/index.tsx index 904ecf43b2b2..bdf772edcaef 100644 --- a/static/app/views/issueDetails/activitySection/index.tsx +++ b/static/app/views/issueDetails/activitySection/index.tsx @@ -9,21 +9,17 @@ import {addSuccessMessage} from 'sentry/actionCreators/indicator'; import {TimeSince} from 'sentry/components/timeSince'; import {IconChat, IconEllipsis} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; -import { - GroupActivityType, - SEER_ACTIVITY_TYPES, - type Group, - type GroupActivity, -} from 'sentry/types/group'; +import type {Group, GroupActivity} from 'sentry/types/group'; import {trackAnalytics} from 'sentry/utils/analytics'; import {uniqueId} from 'sentry/utils/guid'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {ActivityLine} from 'sentry/views/issueDetails/activitySection/activityLineItem'; import { - collapseSeerActivityPairs, - type ActivityFeedItem, + buildActivityFeedItems, + type DisplayedActivityFeedItem, } from 'sentry/views/issueDetails/activitySection/activityLineItem/activityFeedItem'; +import {CollapsedStatusActivityRow} from 'sentry/views/issueDetails/activitySection/activityLineItem/collapsedStatusActivityRow'; import {ActivityLineList} from 'sentry/views/issueDetails/activitySection/activityLineItem/layout'; import { ActivityLineNote, @@ -40,7 +36,7 @@ interface ActivityFeedRowProps { group: Group; handleDelete: (item: GroupActivity) => Promise; inputVariant: 'compact' | 'full'; - item: ActivityFeedItem; + item: DisplayedActivityFeedItem; onCommentEdited?: (activity: GroupActivity[]) => void; showConnector?: boolean; timestampUnitStyle?: React.ComponentProps['unitStyle']; @@ -55,6 +51,24 @@ function ActivityFeedRow({ showConnector, timestampUnitStyle, }: ActivityFeedRowProps) { + if (item.type === 'collapsed_status_activities') { + return ( + + {item.activities.map(activity => ( + + ))} + + ); + } + const {activity} = item; if (!isActivityNote(activity)) { @@ -104,70 +118,6 @@ interface ActivitySectionProps { variant?: 'sidebar' | 'standalone'; } -function isDuplicatePullRequestActivity( - activity: GroupActivity, - adjacentActivity: GroupActivity | undefined -): boolean { - switch (activity.type) { - // REFERENCED_IN_COMMIT should be hidden if there is an adjacent PULL_REQUEST_MERGED activity with the same pull request - case GroupActivityType.REFERENCED_IN_COMMIT: { - if (adjacentActivity?.type !== GroupActivityType.PULL_REQUEST_MERGED) { - return false; - } - - const pullRequest = activity.data.commit?.pullRequest; - const adjacentPullRequest = adjacentActivity.data.pullRequest; - if (!pullRequest || !adjacentPullRequest) { - return false; - } - - return ( - pullRequest.id === adjacentPullRequest.id && - pullRequest.repository.id === adjacentPullRequest.repository.id - ); - } - case GroupActivityType.SEER_PR_CREATED: { - if (adjacentActivity?.type !== GroupActivityType.SET_RESOLVED_IN_PULL_REQUEST) { - return false; - } - - const adjacentPullRequest = adjacentActivity.data.pullRequest; - if (!adjacentPullRequest) { - return false; - } - - return Boolean( - activity.data.pull_requests?.some( - pullRequest => - pullRequest.pull_request.pr_url === adjacentPullRequest.externalUrl - ) - ); - } - default: - return false; - } -} - -function removeAdjacentDuplicatePullRequestActivities(activities: GroupActivity[]): { - activities: GroupActivity[]; - actorActivityById: Map; -} { - const actorActivityById = new Map(); - const filteredActivities = activities.filter((activity, index) => { - const duplicateActivity = [activities[index - 1], activities[index + 1]].find( - adjacentActivity => isDuplicatePullRequestActivity(activity, adjacentActivity) - ); - - if (activity.type === GroupActivityType.SEER_PR_CREATED && duplicateActivity) { - actorActivityById.set(duplicateActivity.id, activity); - } - - return !duplicateActivity; - }); - - return {activities: filteredActivities, actorActivityById}; -} - export function ActivitySection({ group, activities: providedActivities, @@ -210,6 +160,7 @@ export function ActivitySection({ [activities, mutators, onCommentDeleted, organization] ); + const isStandalone = variant === 'standalone'; const activityLink = { pathname: `${baseUrl}${TabPaths[Tab.ACTIVITY]}`, query: { @@ -225,26 +176,23 @@ export function ActivitySection({ }, }; - const showSeerActivities = organization.features.includes( - 'display-seer-actions-as-issue-activities' - ); - const visibleActivities = showSeerActivities - ? activities - : activities.filter(item => !SEER_ACTIVITY_TYPES.has(item.type)); - - const {activities: deduplicatedActivities, actorActivityById} = - removeAdjacentDuplicatePullRequestActivities(visibleActivities); - const filteredActivities = deduplicatedActivities.filter( - item => !filterComments || item.type === GroupActivityType.NOTE - ); - const displayedActivities = collapseSeerActivityPairs(filteredActivities).map(item => { - const actorActivity = actorActivityById.get(item.activity.id); - return item.type === 'activity' && actorActivity ? {...item, actorActivity} : item; + const displayedActivities = buildActivityFeedItems({ + activities, + filterComments, + showSeerActivities: organization.features.includes( + 'display-seer-actions-as-issue-activities' + ), + showStatusFlappingRollups: organization.features.includes( + 'issue-activity-status-flapping-rollup' + ), }); - const inputVariant = variant === 'sidebar' ? 'compact' : 'full'; - const timestampUnitStyle = variant === 'sidebar' ? 'short' : undefined; + const inputVariant = isStandalone ? 'full' : 'compact'; + const timestampUnitStyle = isStandalone ? undefined : 'short'; - const renderActivityItem = (item: ActivityFeedItem, showConnector: boolean) => ( + const renderActivityItem = ( + item: DisplayedActivityFeedItem, + showConnector: boolean + ) => ( ); - if (variant === 'standalone') { + if (isStandalone) { return ( {noteInput}