Skip to content
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

feat: Collect new statistics for the Contact Identification feature #33895

Merged
merged 22 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fc0aee6
feat: add contact identification statistics
matheusbsilva137 Nov 6, 2024
f76a17e
fix: Invalid association between visitors and contacts. (#33868)
pierre-lehnen-rc Nov 5, 2024
8a05375
add totalConflicts statistic
matheusbsilva137 Nov 6, 2024
0b39f91
Create changeset
matheusbsilva137 Nov 6, 2024
63de7e1
fix unit tests
matheusbsilva137 Nov 6, 2024
ffffd12
set default value for counts
matheusbsilva137 Nov 6, 2024
8bd19d9
fix default stats values
matheusbsilva137 Nov 6, 2024
b3e3ef5
return manager information on contacts.search
pierre-lehnen-rc Nov 6, 2024
57b1c18
fix: apps-engine room converter dropping contactId attribute (#33894)
dougfabris Nov 6, 2024
8408bd6
Use estimatedDocumentCount
matheusbsilva137 Nov 7, 2024
91a72c4
fix lint
matheusbsilva137 Nov 7, 2024
2b74127
Combine aggregated statistics in a single DB call
matheusbsilva137 Nov 7, 2024
7555c46
improve: move counts to model
matheusbsilva137 Nov 8, 2024
7df17b0
Use verified flag to compute statistic
matheusbsilva137 Nov 8, 2024
b2dee15
fix lint
matheusbsilva137 Nov 27, 2024
1add186
fix lint and typecheck
matheusbsilva137 Nov 27, 2024
7940732
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Nov 29, 2024
dbf9ff3
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Dec 19, 2024
0c18940
Apply suggestions from code review
matheusbsilva137 Dec 19, 2024
9308132
use more readSecondaryPreferred
matheusbsilva137 Dec 19, 2024
96aeba0
update info log
matheusbsilva137 Dec 19, 2024
8ba58ce
Merge branch 'develop' into feat/add-contact-identification-stats
matheusbsilva137 Dec 19, 2024
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
21 changes: 21 additions & 0 deletions .changeset/perfect-ties-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor
---

Adds statistics related to the new **Contact Identification** feature:
- `totalContacts`: Total number of contacts;
- `totalUnknownContacts`: Total number of unknown contacts;
- `totalMergedContacts`: Total number of merged contacts;
- `totalConflicts`: Total number of merge conflicts;
- `totalResolvedConflicts`: Total number of resolved conflicts;
- `totalBlockedContacts`: Total number of blocked contacts;
- `totalPartiallyBlockedContacts`: Total number of partially blocked contacts;
- `totalFullyBlockedContacts`: Total number of fully blocked contacts;
- `totalVerifiedContacts`: Total number of verified contacts;
- `avgChannelsPerContact`: Average number of channels per contact;
- `totalContactsWithoutChannels`: Number of contacts without channels;
- `totalImportedContacts`: Total number of imported contacts;
- `totalUpsellViews`: Total number of "Advanced Contact Management" Upsell CTA views;
- `totalUpsellClicks`: Total number of "Advanced Contact Management" Upsell CTA clicks;
11 changes: 9 additions & 2 deletions apps/meteor/app/importer/server/classes/Importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ImportDataConverter } from './ImportDataConverter';
import type { ConverterOptions } from './ImportDataConverter';
import { ImporterProgress } from './ImporterProgress';
import { ImporterWebsocket } from './ImporterWebsocket';
import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { t } from '../../../utils/lib/i18n';
import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep';
import type { ImporterInfo } from '../definitions/ImporterInfo';
Expand Down Expand Up @@ -183,6 +183,13 @@ export class Importer {
}
};

const afterContactsBatchFn = async (successCount: number) => {
const { value } = await Settings.incrementValueById('Contacts_Importer_Count', successCount, { returnDocument: 'after' });
if (value) {
void notifyOnSettingChanged(value);
}
};

const onErrorFn = async () => {
await this.addCountCompleted(1);
};
Expand All @@ -197,7 +204,7 @@ export class Importer {
await this.converter.convertUsers({ beforeImportFn, afterImportFn, onErrorFn, afterBatchFn });

await this.updateProgress(ProgressStep.IMPORTING_CONTACTS);
await this.converter.convertContacts({ beforeImportFn, afterImportFn, onErrorFn });
await this.converter.convertContacts({ beforeImportFn, afterImportFn, onErrorFn, afterBatchFn: afterContactsBatchFn });

await this.updateProgress(ProgressStep.IMPORTING_CHANNELS);
await this.converter.convertChannels(startedByUserId, { beforeImportFn, afterImportFn, onErrorFn });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export class RecordConverter<R extends IImportRecord, T extends RecordConverterO

protected failedCount = 0;

protected newCount = 0;

public aborted = false;

constructor(options?: T, logger?: Logger, cache?: ConverterCache) {
Expand Down Expand Up @@ -194,11 +196,13 @@ export class RecordConverter<R extends IImportRecord, T extends RecordConverterO
afterImportFn,
onErrorFn,
processRecord,
afterBatchFn,
}: IConversionCallbacks & { processRecord?: (record: R) => Promise<boolean | undefined> } = {}): Promise<void> {
const records = await this.getDataToImport();

this.skippedCount = 0;
this.failedCount = 0;
this.newCount = 0;

for await (const record of records) {
const { _id } = record;
Expand All @@ -214,8 +218,11 @@ export class RecordConverter<R extends IImportRecord, T extends RecordConverterO

const isNew = await (processRecord || this.convertRecord).call(this, record);

if (typeof isNew === 'boolean' && afterImportFn) {
await afterImportFn(record, isNew);
if (typeof isNew === 'boolean') {
this.newCount++;
if (afterImportFn) {
await afterImportFn(record, isNew);
}
}
} catch (e) {
this.failedCount++;
Expand All @@ -225,6 +232,9 @@ export class RecordConverter<R extends IImportRecord, T extends RecordConverterO
}
}
}
if (afterBatchFn) {
await afterBatchFn(this.newCount, this.failedCount);
}
}

async convertData(callbacks: IConversionCallbacks = {}): Promise<void> {
Expand Down
21 changes: 17 additions & 4 deletions apps/meteor/app/livechat/server/lib/contacts/updateContact.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/core-typings';
import { LivechatContacts, LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models';
import { LivechatContacts, LivechatInquiry, LivechatRooms, Settings, Subscriptions } from '@rocket.chat/models';

import { getAllowedCustomFields } from './getAllowedCustomFields';
import { validateContactManager } from './validateContactManager';
Expand All @@ -8,6 +8,7 @@ import {
notifyOnSubscriptionChangedByVisitorIds,
notifyOnRoomChangedByContactId,
notifyOnLivechatInquiryChangedByVisitorIds,
notifyOnSettingChanged,
} from '../../../../lib/server/lib/notifyListener';

export type UpdateContactParams = {
Expand All @@ -24,9 +25,12 @@ export type UpdateContactParams = {
export async function updateContact(params: UpdateContactParams): Promise<ILivechatContact> {
const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels, wipeConflicts } = params;

const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'customFields'>>(contactId, {
projection: { _id: 1, name: 1, customFields: 1 },
});
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'customFields' | 'conflictingFields'>>(
contactId,
{
projection: { _id: 1, name: 1, customFields: 1, conflictingFields: 1 },
},
);

if (!contact) {
throw new Error('error-contact-not-found');
Expand All @@ -36,6 +40,15 @@ export async function updateContact(params: UpdateContactParams): Promise<ILivec
await validateContactManager(contactManager);
}

if (wipeConflicts && contact.conflictingFields?.length) {
const { value } = await Settings.incrementValueById('Resolved_Conflicts_Count', contact.conflictingFields.length, {
returnDocument: 'after',
});
if (value) {
void notifyOnSettingChanged(value);
}
}

const workspaceAllowedCustomFields = await getAllowedCustomFields();
const workspaceAllowedCustomFieldsIds = workspaceAllowedCustomFields.map((customField) => customField._id);
const currentCustomFieldsIds = Object.keys(contact.customFields || {});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { IStats } from '@rocket.chat/core-typings';
import { LivechatContacts } from '@rocket.chat/models';

import { settings } from '../../../settings/server';

export async function getContactVerificationStatistics(): Promise<IStats['contactVerification']> {
const [
totalContacts,
totalUnknownContacts,
[{ totalConflicts, avgChannelsPerContact } = { totalConflicts: 0, avgChannelsPerContact: 0 }],
totalBlockedContacts,
totalFullyBlockedContacts,
totalVerifiedContacts,
totalContactsWithoutChannels,
] = await Promise.all([
LivechatContacts.estimatedDocumentCount(),
LivechatContacts.countUnknown(),
LivechatContacts.getStatistics().toArray(),
LivechatContacts.countBlocked(),
LivechatContacts.countFullyBlocked(),
LivechatContacts.countVerified(),
LivechatContacts.countContactsWithoutChannels(),
]);

return {
totalContacts,
totalUnknownContacts,
totalMergedContacts: settings.get('Merged_Contacts_Count'),
totalConflicts,
totalResolvedConflicts: settings.get('Resolved_Conflicts_Count'),
totalBlockedContacts,
totalPartiallyBlockedContacts: totalBlockedContacts - totalFullyBlockedContacts,
totalFullyBlockedContacts,
totalVerifiedContacts,
avgChannelsPerContact,
totalContactsWithoutChannels,
totalImportedContacts: settings.get('Contacts_Importer_Count'),
totalUpsellViews: settings.get('Advanced_Contact_Upsell_Views_Count'),
totalUpsellClicks: settings.get('Advanced_Contact_Upsell_Clicks_Count'),
};
}
2 changes: 2 additions & 0 deletions apps/meteor/app/statistics/server/lib/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { MongoInternals } from 'meteor/mongo';
import moment from 'moment';

import { getAppsStatistics } from './getAppsStatistics';
import { getContactVerificationStatistics } from './getContactVerificationStatistics';
import { getStatistics as getEnterpriseStatistics } from './getEEStatistics';
import { getImporterStatistics } from './getImporterStatistics';
import { getServicesStatistics } from './getServicesStatistics';
Expand Down Expand Up @@ -477,6 +478,7 @@ export const statistics = {
statistics.services = await getServicesStatistics();
statistics.importer = getImporterStatistics();
statistics.videoConf = await VideoConf.getStatistics();
statistics.contactVerification = await getContactVerificationStatistics();

// If getSettingsStatistics() returns an error, save as empty object.
statsPms.push(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRole } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useRole, useEndpoint } from '@rocket.chat/ui-contexts';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';

import { getURL } from '../../../../app/utils/client/getURL';
Expand All @@ -18,6 +18,22 @@ const AdvancedContactModal = ({ onCancel }: AdvancedContactModalProps) => {
const hasLicense = useHasLicenseModule('contact-id-verification') as boolean;
const { shouldShowUpsell, handleManageSubscription } = useUpsellActions(hasLicense);
const openExternalLink = useExternalLink();
const eventStats = useEndpoint('POST', '/v1/statistics.telemetry');

const handleUpsellClick = async () => {
eventStats({
params: [{ eventName: 'updateCounter', settingsId: 'Advanced_Contact_Upsell_Clicks_Count' }],
});
return handleManageSubscription();
};

useEffect(() => {
if (shouldShowUpsell) {
eventStats({
params: [{ eventName: 'updateCounter', settingsId: 'Advanced_Contact_Upsell_Views_Count' }],
});
}
}, [eventStats, shouldShowUpsell]);

return (
<GenericUpsellModal
Expand All @@ -27,7 +43,7 @@ const AdvancedContactModal = ({ onCancel }: AdvancedContactModalProps) => {
onClose={onCancel}
onCancel={shouldShowUpsell ? onCancel : () => openExternalLink('https://go.rocket.chat/i/omnichannel-docs')}
cancelText={!shouldShowUpsell ? t('Learn_more') : undefined}
onConfirm={shouldShowUpsell ? handleManageSubscription : undefined}
onConfirm={shouldShowUpsell ? handleUpsellClick : undefined}
annotation={!shouldShowUpsell && !isAdmin ? t('Ask_enable_advanced_contact_profile') : undefined}
/>
);
Expand Down
15 changes: 11 additions & 4 deletions apps/meteor/ee/server/patches/mergeContacts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ILivechatContact, ILivechatContactChannel, ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { LivechatContacts, LivechatRooms } from '@rocket.chat/models';
import { LivechatContacts, LivechatRooms, Settings } from '@rocket.chat/models';
import type { ClientSession } from 'mongodb';

import { notifyOnSettingChanged } from '../../../app/lib/server/lib/notifyListener';
import { isSameChannel } from '../../../app/livechat/lib/isSameChannel';
import { ContactMerger } from '../../../app/livechat/server/lib/contacts/ContactMerger';
import { mergeContacts } from '../../../app/livechat/server/lib/contacts/mergeContacts';
Expand Down Expand Up @@ -41,10 +42,16 @@ export const runMergeContacts = async (

const similarContactIds = similarContacts.map((c) => c._id);
const { deletedCount } = await LivechatContacts.deleteMany({ _id: { $in: similarContactIds } }, { session });

const { value } = await Settings.incrementValueById('Merged_Contacts_Count', similarContacts.length, { returnDocument: 'after' });
if (value) {
void notifyOnSettingChanged(value);
}
logger.info({
msg: `${deletedCount} contacts have been deleted and merged`,
deletedContactIds: similarContactIds,
contactId,
msg: 'contacts have been deleted and merged with a contact',
similarContactIds,
deletedCount,
originalContactId: originalContact._id,
});

logger.debug({ msg: 'Updating rooms with new contact id', contactId });
Expand Down
25 changes: 25 additions & 0 deletions apps/meteor/ee/server/settings/contact-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ export const addSettings = async (): Promise<void> => {
const omnichannelEnabledQuery = { _id: 'Livechat_enabled', value: true };

return settingsRegistry.addGroup('Omnichannel', async function () {
await this.add('Merged_Contacts_Count', 0, {
type: 'int',
hidden: true,
});

await this.add('Resolved_Conflicts_Count', 0, {
type: 'int',
hidden: true,
});

await this.add('Contacts_Importer_Count', 0, {
type: 'int',
hidden: true,
});

await this.add('Advanced_Contact_Upsell_Views_Count', 0, {
type: 'int',
hidden: true,
});

await this.add('Advanced_Contact_Upsell_Clicks_Count', 0, {
type: 'int',
hidden: true,
});

return this.with(
{
enterprise: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const modelsMock = {
LivechatRooms: {
updateMergedContactIds: sinon.stub(),
},
Settings: {
incrementValueById: sinon.stub(),
},
};

const contactMergerStub = {
Expand All @@ -22,6 +25,7 @@ const { runMergeContacts } = proxyquire.noCallThru().load('../../../../../../ser
'../../../app/livechat/server/lib/contacts/mergeContacts': { mergeContacts: { patch: sinon.stub() } },
'../../../app/livechat/server/lib/contacts/ContactMerger': { ContactMerger: contactMergerStub },
'../../../app/livechat-enterprise/server/lib/logger': { logger: { info: sinon.stub(), debug: sinon.stub() } },
'../../../app/lib/server/lib/notifyListener': { notifyOnSettingChanged: sinon.stub() },
'@rocket.chat/models': modelsMock,
});

Expand All @@ -45,6 +49,7 @@ describe('mergeContacts', () => {
modelsMock.LivechatContacts.findSimilarVerifiedContacts.reset();
modelsMock.LivechatContacts.deleteMany.reset();
modelsMock.LivechatRooms.updateMergedContactIds.reset();
modelsMock.Settings.incrementValueById.reset();
contactMergerStub.getAllFieldsFromContact.reset();
contactMergerStub.mergeFieldsIntoContact.reset();
modelsMock.LivechatContacts.deleteMany.resolves({ deletedCount: 0 });
Expand Down Expand Up @@ -102,6 +107,7 @@ describe('mergeContacts', () => {

modelsMock.LivechatContacts.findOneById.resolves(originalContact);
modelsMock.LivechatContacts.findSimilarVerifiedContacts.resolves([similarContact]);
modelsMock.Settings.incrementValueById.resolves({ value: undefined });

await runMergeContacts(() => undefined, 'contactId', { visitorId: 'visitorId', source: { type: 'sms' } });

Expand All @@ -114,5 +120,6 @@ describe('mergeContacts', () => {

expect(modelsMock.LivechatContacts.deleteMany.calledOnceWith({ _id: { $in: ['differentId'] } })).to.be.true;
expect(modelsMock.LivechatRooms.updateMergedContactIds.calledOnceWith(['differentId'], 'contactId')).to.be.true;
expect(modelsMock.Settings.incrementValueById.calledOnceWith('Merged_Contacts_Count', 1)).to.be.true;
});
});
Loading
Loading