diff --git a/backend/src/bin/jobs/memberScoreCoordinator.ts b/backend/src/bin/jobs/memberScoreCoordinator.ts index 5807a818c8..85f5c60497 100644 --- a/backend/src/bin/jobs/memberScoreCoordinator.ts +++ b/backend/src/bin/jobs/memberScoreCoordinator.ts @@ -5,7 +5,7 @@ import { PythonWorkerMessageType } from '../../serverless/types/workerTypes' const job: CrowdJob = { name: 'Member Score Coordinator', - cronTime: cronGenerator.every(20).minutes(), + cronTime: cronGenerator.every(90).minutes(), onTrigger: async () => { await sendPythonWorkerMessage('global', { type: PythonWorkerMessageType.MEMBERS_SCORE, diff --git a/backend/src/bin/nodejs-worker.ts b/backend/src/bin/nodejs-worker.ts index be56c99bcb..1729d49bee 100644 --- a/backend/src/bin/nodejs-worker.ts +++ b/backend/src/bin/nodejs-worker.ts @@ -1,4 +1,4 @@ -import { Logger, getChildLogger, getServiceLogger } from '@crowd/logging' +import { Logger, getChildLogger, getServiceLogger, logExecutionTimeV2 } from '@crowd/logging' import { DeleteMessageRequest, Message, ReceiveMessageRequest } from 'aws-sdk/clients/sqs' import moment from 'moment' import { timeout } from '@crowd/common' @@ -119,7 +119,19 @@ async function handleMessages() { }) try { - messageLogger.debug('Received a new queue message!') + if ( + msg.type === NodeWorkerMessageType.DB_OPERATIONS && + (msg as any).operation === 'update_members' + ) { + messageLogger.warn('Skipping update_members message! TEMPORARY MEASURE!') + await removeFromQueue(message.ReceiptHandle) + return + } + + messageLogger.info( + { messageType: msg.type, messagePayload: JSON.stringify(msg) }, + 'Received a new queue message!', + ) let processFunction: (msg: NodeWorkerMessageBase, logger?: Logger) => Promise let keep = false @@ -152,7 +164,13 @@ async function handleMessages() { await removeFromQueue(message.ReceiptHandle) messagesInProgress.set(message.MessageId, msg) try { - await processFunction(msg, messageLogger) + await logExecutionTimeV2( + async () => { + await processFunction(msg, messageLogger) + }, + messageLogger, + 'queueMessageProcessingTime', + ) } catch (err) { messageLogger.error(err, 'Error while processing queue message!') } finally { diff --git a/backend/src/cubejs/Dockerfile b/backend/src/cubejs/Dockerfile index 1e27440d18..264a3125cb 100644 --- a/backend/src/cubejs/Dockerfile +++ b/backend/src/cubejs/Dockerfile @@ -1,3 +1,3 @@ -FROM cubejs/cube:v0.32.16 +FROM cubejs/cube:v0.33.55 -COPY ./ /cube/conf/ \ No newline at end of file +COPY ./ /cube/conf/ diff --git a/backend/src/database/migrations/U1694760454__segment-activity-channels-missing-index.sql b/backend/src/database/migrations/U1694760454__segment-activity-channels-missing-index.sql new file mode 100644 index 0000000000..35ef6cfc58 --- /dev/null +++ b/backend/src/database/migrations/U1694760454__segment-activity-channels-missing-index.sql @@ -0,0 +1 @@ +drop index if exists "ix_segmentActivityChannels_segmentId_platform"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1694760454__segment-activity-channels-missing-index.sql b/backend/src/database/migrations/V1694760454__segment-activity-channels-missing-index.sql new file mode 100644 index 0000000000..b7b5b869b4 --- /dev/null +++ b/backend/src/database/migrations/V1694760454__segment-activity-channels-missing-index.sql @@ -0,0 +1 @@ +create index if not exists "ix_segmentActivityChannels_segmentId_platform" on "segmentActivityChannels" ("segmentId", platform); \ No newline at end of file diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index 5da9330161..9ef90ecc2d 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -1152,7 +1152,7 @@ class OrganizationRepository { const query = ` with leaf_segment_ids as (${segmentsSubQuery}), member_data as (select a."organizationId", - count(distinct m.id) filter ( where mo."dateEnd" is null ) as "memberCount", + count(distinct a."memberId") as "memberCount", count(distinct a.id) as "activityCount", case when array_agg(distinct a.platform) = array [null] then array []::text[] @@ -1160,13 +1160,11 @@ class OrganizationRepository { max(a.timestamp) as "lastActive", min(a.timestamp) filter ( where a.timestamp <> '1970-01-01T00:00:00.000Z' ) as "joinedAt" from leaf_segment_ids ls - left join activities a + join activities a on a."segmentId" = ls.id and a."organizationId" = :id and a."deletedAt" is null - left join members m on a."memberId" = m.id and m."deletedAt" is null - left join "memberOrganizations" mo - on m.id = mo."memberId" and mo."organizationId" = :id - left join "memberIdentities" mi on m.id = mi."memberId" + join members m on a."memberId" = m.id and m."deletedAt" is null + join "memberOrganizations" mo on m.id = mo."memberId" and mo."organizationId" = :id and mo."dateEnd" is null group by a."organizationId"), organization_segments as (select "organizationId", array_agg("segmentId") as "segments" from "organizationSegments" diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts index e056c9bb25..0ca5777f88 100644 --- a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts +++ b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts @@ -1,7 +1,10 @@ import htmlToMrkdwn from 'html-to-mrkdwn-ts' -import { integrationLabel } from '@crowd/types' +import { integrationLabel, integrationProfileUrl } from '@crowd/types' import { API_CONFIG } from '../../../../../../conf' +const defaultAvatarUrl = + 'https://uploads-ssl.webflow.com/635150609746eee5c60c4aac/6502afc9d75946873c1efa93_image%20(292).png' + const computeEngagementLevel = (score) => { if (score <= 1) { return 'Silent' @@ -47,45 +50,61 @@ const truncateText = (text: string, characters: number = 60): string => { } export const newActivityBlocks = (activity) => { - const display = htmlToMrkdwn(replaceHeadline(`${activity.display.default}`)) + const display = htmlToMrkdwn(replaceHeadline(activity.display.default)) const reach = activity.member.reach?.[activity.platform] || activity.member.reach?.total + + const { member } = activity const memberProperties = [] - if (activity.member.attributes.jobTitle?.default) { - memberProperties.push(activity.member.attributes.jobTitle?.default) + if (member.attributes.jobTitle?.default) { + memberProperties.push(`*💼 Job title:* ${member.attributes.jobTitle?.default}`) + } + if (member.organizations.length > 0) { + const orgs = member.organizations.map( + (org) => + `<${`${API_CONFIG.frontendUrl}/organizations/${org.id}`}|${org.name || org.displayName}>`, + ) + memberProperties.push(`*🏢 Organization:* ${orgs.join(' | ')}`) } if (reach > 0) { - memberProperties.push(`${reach} followers`) + memberProperties.push(`*👥 Reach:* ${reach} followers`) + } + if (member.attributes?.location?.default) { + memberProperties.push(`*📍 Location:* ${member.attributes?.location?.default}`) + } + if (member.emails.length > 0) { + const [email] = member.emails + memberProperties.push(`*✉️ Email:* `) } const engagementLevel = computeEngagementLevel(activity.member.score || activity.engagement) if (engagementLevel.length > 0) { - memberProperties.push(`*Engagement level:* ${engagementLevel}`) + memberProperties.push(`*📊 Engagement level:* ${engagementLevel}`) } if (activity.member.activeOn) { const platforms = activity.member.activeOn .map((platform) => integrationLabel[platform] || platform) .join(' | ') - memberProperties.push(`*Active on:* ${platforms}`) + memberProperties.push(`*💬 Active on:* ${platforms}`) } + const profiles = Object.keys(member.username) + .map((p) => { + const username = (member.username?.[p] || []).length > 0 ? member.username[p][0] : null + const url = + member.attributes?.url?.[p] || (username && integrationProfileUrl[p](username)) || null + return { + platform: p, + url, + } + }) + .filter((p) => !!p.url) + return { blocks: [ { type: 'section', text: { type: 'mrkdwn', - text: ':satellite_antenna: *New activity*', - }, - }, - { - type: 'divider', - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*<${API_CONFIG.frontendUrl}/members/${activity.member.id}|${ - activity.member.displayName - }>* \n *${truncateText(display.text)}*`, + text: `*<${API_CONFIG.frontendUrl}/members/${activity.member.id}|${activity.member.displayName}>* *${display.text}*`, }, ...(activity.url ? { @@ -105,37 +124,72 @@ export const newActivityBlocks = (activity) => { } : {}), }, + ...(activity.title || activity.body + ? [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `>${ + activity.title && activity.title !== activity.display.default + ? `*${truncateText(htmlToMrkdwn(activity.title).text, 120).replaceAll( + '\n', + '\n>', + )}* \n>` + : '' + }${truncateText(htmlToMrkdwn(activity.body).text, 260).replaceAll('\n', '\n>')}`, + }, + }, + ] + : []), + ...(memberProperties.length > 0 + ? [ + { + type: 'divider', + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: memberProperties.join('\n'), + }, + accessory: { + type: 'image', + image_url: member.attributes?.avatarUrl?.default ?? defaultAvatarUrl, + alt_text: 'computer thumbnail', + }, + }, + ] + : []), { - type: 'context', + type: 'actions', elements: [ { - type: 'mrkdwn', - text: memberProperties.join(' • '), + type: 'button', + text: { + type: 'plain_text', + text: 'View in crowd.dev', + emoji: true, + }, + url: `${API_CONFIG.frontendUrl}/members/${member.id}`, }, + ...(profiles.length > 0 + ? [ + { + type: 'overflow', + options: profiles.map(({ platform, url }) => ({ + text: { + type: 'plain_text', + text: `${integrationLabel[platform] ?? platform} profile`, + emoji: true, + }, + url, + })), + }, + ] + : []), ], }, ], - ...(activity.title || activity.body - ? { - attachments: [ - { - color: '#eeeeee', - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: `${ - activity.title && activity.title !== activity.display.default - ? `*${htmlToMrkdwn(activity.title).text}* \n ` - : '' - }${htmlToMrkdwn(activity.body).text}`, - }, - }, - ], - }, - ], - } - : {}), } } diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts index 50a386bfa0..87d5cd71f1 100644 --- a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts +++ b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts @@ -1,19 +1,49 @@ -import { integrationLabel } from '@crowd/types' +import { integrationLabel, integrationProfileUrl } from '@crowd/types' import { API_CONFIG } from '../../../../../../conf' +const defaultAvatarUrl = + 'https://uploads-ssl.webflow.com/635150609746eee5c60c4aac/6502afc9d75946873c1efa93_image%20(292).png' + export const newMemberBlocks = (member) => { const platforms = member.activeOn const reach = platforms && platforms.length > 0 ? member.reach?.[platforms[0]] : member.reach?.total + const details = [] + if (member.attributes.jobTitle?.default) { + details.push(`*💼 Job title:* ${member.attributes.jobTitle?.default}`) + } + if (member.organizations.length > 0) { + const orgs = member.organizations.map( + (org) => + `<${`${API_CONFIG.frontendUrl}/organizations/${org.id}`}|${org.name || org.displayName}>`, + ) + details.push(`*🏢 Organization:* ${orgs.join(' | ')}`) + } + if (reach > 0) { + details.push(`*👥 Reach:* ${reach} followers`) + } + if (member.attributes?.location?.default) { + details.push(`*📍 Location:* ${member.attributes?.location?.default}`) + } + if (member.emails.length > 0) { + const [email] = member.emails + details.push(`*✉️ Email:* `) + } + const profiles = Object.keys(member.username) + .filter((p) => !platforms.includes(p)) + .map((p) => { + const username = (member.username?.[p] || []).length > 0 ? member.username[p][0] : null + const url = + member.attributes?.url?.[p] || (username && integrationProfileUrl[p](username)) || null + return { + platform: p, + url, + } + }) + .filter((p) => !!p.url) + return { blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: ':tada: *New member*', - }, - }, { type: 'header', text: { @@ -37,85 +67,22 @@ export const newMemberBlocks = (member) => { }, ] : []), - { - type: 'divider', - }, - ...(member.attributes.jobTitle?.default - ? [ - { - type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Title/Role:*', - }, - { - type: 'mrkdwn', - text: member.attributes.jobTitle?.default || '-', - }, - ], - }, - { - type: 'divider', - }, - ] - : []), - ...(member.organizations.length > 0 - ? [ - { - type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Organization:*', - }, - { - type: 'mrkdwn', - text: `<${`${API_CONFIG.frontendUrl}/organizations/${member.organizations[0].id}`}|${ - member.organizations[0].name - }>`, - }, - ], - }, - { - type: 'divider', - }, - ] - : []), - ...(reach > 0 + ...(details.length > 0 ? [ - { - type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Followers:*', - }, - { - type: 'mrkdwn', - text: reach > 0 ? `${reach}` : '-', - }, - ], - }, { type: 'divider', }, - ] - : []), - ...(member.attributes?.location?.default - ? [ { type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Location:*', - }, - { - type: 'mrkdwn', - text: member.attributes?.location?.default || '-', - }, - ], + text: { + type: 'mrkdwn', + text: details.length > 0 ? details.join('\n') : '\n', + }, + accessory: { + type: 'image', + image_url: member.attributes?.avatarUrl?.default ?? defaultAvatarUrl, + alt_text: 'computer thumbnail', + }, }, { type: 'divider', @@ -139,12 +106,27 @@ export const newMemberBlocks = (member) => { type: 'button', text: { type: 'plain_text', - text: `View ${integrationLabel[platform]} profile`, + text: `${integrationLabel[platform] ?? platform} profile`, emoji: true, }, url: member.attributes?.url?.[platform], })) .filter((action) => !!action.url), + ...(profiles.length > 0 + ? [ + { + type: 'overflow', + options: profiles.map(({ platform, url }) => ({ + text: { + type: 'plain_text', + text: `${integrationLabel[platform] ?? platform} profile`, + emoji: true, + }, + url, + })), + }, + ] + : []), ], }, ], diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 6859a51700..94eb75bf47 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -46,7 +46,6 @@ declare module '@vue/runtime-core' { ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] - ElTextarea: typeof import('element-plus/es')['ElTextarea'] ElTimeline: typeof import('element-plus/es')['ElTimeline'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect'] diff --git a/frontend/src/modules/activity/config/filters/activityType/ActivityTypeFilter.vue b/frontend/src/modules/activity/config/filters/activityType/ActivityTypeFilter.vue index 75eab8a052..b648497301 100644 --- a/frontend/src/modules/activity/config/filters/activityType/ActivityTypeFilter.vue +++ b/frontend/src/modules/activity/config/filters/activityType/ActivityTypeFilter.vue @@ -1,20 +1,20 @@