Skip to content

Commit

Permalink
chore: prepare start and endRetrospective for recurrence
Browse files Browse the repository at this point in the history
Only refactored part of the functionality into different functions.
  • Loading branch information
Dschoordsch committed Jan 10, 2024
1 parent dc6be15 commit 2d4e20a
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 280 deletions.
216 changes: 4 additions & 212 deletions packages/server/graphql/mutations/endRetrospective.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,11 @@
import {GraphQLID, GraphQLNonNull} from 'graphql'
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
import {DISCUSS, PARABOL_AI_USER_ID} from 'parabol-client/utils/constants'
import getMeetingPhase from 'parabol-client/utils/getMeetingPhase'
import findStageById from 'parabol-client/utils/meetings/findStageById'
import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck'
import getRethink from '../../database/rethinkDriver'
import {RDatum} from '../../database/stricterR'
import MeetingRetrospective from '../../database/types/MeetingRetrospective'
import TimelineEventRetroComplete from '../../database/types/TimelineEventRetroComplete'
import getKysely from '../../postgres/getKysely'
import removeSuggestedAction from '../../safeMutations/removeSuggestedAction'
import {analytics} from '../../utils/analytics/analytics'
import {getUserId, isTeamMember} from '../../utils/authorization'
import getPhase from '../../utils/getPhase'
import publish from '../../utils/publish'
import RecallAIServerManager from '../../utils/RecallAIServerManager'
import sendToSentry from '../../utils/sendToSentry'
import standardError from '../../utils/standardError'
import {GQLContext} from '../graphql'
import EndRetrospectivePayload from '../types/EndRetrospectivePayload'
import updateTeamInsights from './helpers/updateTeamInsights'
import sendNewMeetingSummary from './helpers/endMeeting/sendNewMeetingSummary'
import generateWholeMeetingSentimentScore from './helpers/generateWholeMeetingSentimentScore'
import generateWholeMeetingSummary from './helpers/generateWholeMeetingSummary'
import handleCompletedStage from './helpers/handleCompletedStage'
import {IntegrationNotifier} from './helpers/notifications/IntegrationNotifier'
import removeEmptyTasks from './helpers/removeEmptyTasks'
import updateQualAIMeetingsCount from './helpers/updateQualAIMeetingsCount'
import gatherInsights from './helpers/gatherInsights'

const getTranscription = async (recallBotId?: string | null) => {
if (!recallBotId) return
const manager = new RecallAIServerManager()
return await manager.getBotTranscript(recallBotId)
}

const summarizeRetroMeeting = async (meeting: MeetingRetrospective, context: GQLContext) => {
const {dataLoader, authToken} = context
const {id: meetingId, phases, facilitatorUserId, teamId, recallBotId} = meeting
const r = await getRethink()
const [reflectionGroups, reflections, sentimentScore] = await Promise.all([
dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId),
dataLoader.get('retroReflectionsByMeetingId').load(meetingId),
generateWholeMeetingSentimentScore(meetingId, facilitatorUserId, dataLoader)
])
const discussPhase = getPhase(phases, 'discuss')
const {stages} = discussPhase
const discussionIds = stages.map((stage) => stage.discussionId)

const reflectionGroupIds = reflectionGroups.map(({id}) => id)
const hasTopicSummary = reflectionGroups.some((group) => group.summary)
if (hasTopicSummary) {
const groupsWithMissingTopicSummaries = reflectionGroups.filter((group) => {
const reflectionsInGroup = reflections.filter(
(reflection) => reflection.reflectionGroupId === group.id
)
return reflectionsInGroup.length > 1 && !group.summary
})
if (groupsWithMissingTopicSummaries.length > 0) {
const missingGroupIds = groupsWithMissingTopicSummaries.map(({id}) => id).join(', ')
const error = new Error('Missing AI topic summary')
const viewerId = getUserId(authToken)
sendToSentry(error, {
userId: viewerId,
tags: {missingGroupIds, meetingId}
})
}
}
const [summary, transcription] = await Promise.all([
generateWholeMeetingSummary(discussionIds, meetingId, teamId, facilitatorUserId, dataLoader),
getTranscription(recallBotId)
])

await r
.table('NewMeeting')
.get(meetingId)
.update(
{
commentCount: r
.table('Comment')
.getAll(r.args(discussionIds), {index: 'discussionId'})
.filter((row: RDatum) =>
row('isActive').eq(true).and(row('createdBy').ne(PARABOL_AI_USER_ID))
)
.count()
.default(0) as unknown as number,
taskCount: r
.table('Task')
.getAll(r.args(discussionIds), {index: 'discussionId'})
.count()
.default(0) as unknown as number,
topicCount: reflectionGroupIds.length,
reflectionCount: reflections.length,
sentimentScore,
summary,
transcription
},
{nonAtomic: true}
)
.run()

dataLoader.get('newMeetings').clear(meetingId)
// wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount
sendNewMeetingSummary(meeting, context).catch(console.log)
updateQualAIMeetingsCount(meetingId, teamId, dataLoader)
// wait for meeting stats to be generated before sending Slack notification
IntegrationNotifier.endMeeting(dataLoader, meetingId, teamId)
const data = {meetingId}
const operationId = dataLoader.share()
const subOptions = {operationId}
publish(SubscriptionChannel.MEETING, meetingId, 'EndRetrospectiveSuccess', data, subOptions)
}
import safeEndRetrospective from './helpers/safeEndRetrospective'

export default {
type: new GraphQLNonNull(EndRetrospectivePayload),
Expand All @@ -122,11 +17,8 @@ export default {
}
},
async resolve(_source: unknown, {meetingId}: {meetingId: string}, context: GQLContext) {
const {authToken, socketId: mutatorId, dataLoader} = context
const {authToken} = context
const r = await getRethink()
const pg = getKysely()
const operationId = dataLoader.share()
const subOptions = {mutatorId, operationId}
const now = new Date()
const viewerId = getUserId(authToken)

Expand All @@ -137,7 +29,7 @@ export default {
.default(null)
.run()) as MeetingRetrospective | null
if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId})
const {endedAt, facilitatorStageId, phases, teamId} = meeting
const {endedAt, teamId} = meeting

// VALIDATION
if (!isTeamMember(authToken, teamId) && authToken.rol !== 'su') {
Expand All @@ -146,106 +38,6 @@ export default {
if (endedAt) return standardError(new Error('Meeting already ended'), {userId: viewerId})

// RESOLUTION
const currentStageRes = findStageById(phases, facilitatorStageId)
if (currentStageRes) {
const {stage} = currentStageRes
await handleCompletedStage(stage, meeting, dataLoader)
stage.isComplete = true
stage.endAt = now
}
const phase = getMeetingPhase(phases)

const insights = await gatherInsights(meeting, dataLoader)
const completedRetrospective = (await r
.table('NewMeeting')
.get(meetingId)
.update(
{
endedAt: now,
phases,
...insights
},
{returnChanges: true}
)('changes')(0)('new_val')
.default(null)
.run()) as unknown as MeetingRetrospective

if (!completedRetrospective) {
return standardError(new Error('Completed retrospective meeting does not exist'), {
userId: viewerId
})
}

// remove any empty tasks
const {templateId} = completedRetrospective
const [meetingMembers, team, teamMembers, removedTaskIds, template] = await Promise.all([
dataLoader.get('meetingMembersByMeetingId').load(meetingId),
dataLoader.get('teams').loadNonNull(teamId),
dataLoader.get('teamMembersByTeamId').load(teamId),
removeEmptyTasks(meetingId),
dataLoader.get('meetingTemplates').loadNonNull(templateId),
pg
.deleteFrom('RetroReflectionGroup')
.where('meetingId', '=', meetingId)
.where('isActive', '=', false)
.execute(),
r
.table('RetroReflectionGroup')
.getAll(meetingId, {index: 'meetingId'})
.filter({isActive: false})
.delete()
.run(),
updateTeamInsights(teamId, dataLoader)
])
// wait for removeEmptyTasks before summarizeRetroMeeting
// don't await for the OpenAI response or it'll hang for a while when ending the retro
summarizeRetroMeeting(completedRetrospective, context)
analytics.retrospectiveEnd(completedRetrospective, meetingMembers, template, dataLoader)
checkTeamsLimit(team.orgId, dataLoader)
const events = teamMembers.map(
(teamMember) =>
new TimelineEventRetroComplete({
userId: teamMember.userId,
teamId,
orgId: team.orgId,
meetingId
})
)
const timelineEventId = events[0]!.id
await r.table('TimelineEvent').insert(events).run()

if (team.isOnboardTeam) {
const teamLeadUserId = await r
.table('TeamMember')
.getAll(teamId, {index: 'teamId'})
.filter({isLead: true})
.nth(0)('userId')
.run()

const removedSuggestedActionId = await removeSuggestedAction(
teamLeadUserId,
'tryRetroMeeting'
)
if (removedSuggestedActionId) {
publish(
SubscriptionChannel.NOTIFICATION,
teamLeadUserId,
'EndRetrospectiveSuccess',
{removedSuggestedActionId},
subOptions
)
}
}

const data = {
meetingId,
teamId,
isKill: !!(phase && phase.phaseType !== DISCUSS),
removedTaskIds,
timelineEventId
}
publish(SubscriptionChannel.TEAM, teamId, 'EndRetrospectiveSuccess', data, subOptions)

return data
return safeEndRetrospective({meeting, now, context})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import getRethink from '../../../database/rethinkDriver'
import MeetingRetrospective from '../../../database/types/MeetingRetrospective'
import generateUID from '../../../generateUID'
import {MeetingTypeEnum} from '../../../postgres/types/Meeting'
import {DataLoaderWorker} from '../../graphql'
import createNewMeetingPhases from './createNewMeetingPhases'

export const DEFAULT_PROMPT = 'What are you working on today? Stuck on anything?'

const safeCreateRetrospective = async (
meetingSettings: {
teamId: string
facilitatorUserId: string
totalVotes: number
maxVotesPerGroup: number
disableAnonymity: boolean
templateId: string
videoMeetingURL?: string
},
dataLoader: DataLoaderWorker
) => {
const r = await getRethink()
const {teamId, facilitatorUserId} = meetingSettings
const meetingType: MeetingTypeEnum = 'retrospective'
const [meetingCount, team] = await Promise.all([
r
.table('NewMeeting')
.getAll(teamId, {index: 'teamId'})
.filter({meetingType})
.count()
.default(0)
.run(),
dataLoader.get('teams').loadNonNull(teamId)
])

const organization = await r.table('Organization').get(team.orgId).run()
const {showConversionModal} = organization

const meetingId = generateUID()
const phases = await createNewMeetingPhases(
facilitatorUserId,
teamId,
meetingId,
meetingCount,
meetingType,
dataLoader
)

return new MeetingRetrospective({
id: meetingId,
meetingCount,
phases,
showConversionModal,
...meetingSettings
})
}

export default safeCreateRetrospective
Loading

0 comments on commit 2d4e20a

Please sign in to comment.