Skip to content

Commit

Permalink
feat: Collect new statistics for the Contact Identification feature (#…
Browse files Browse the repository at this point in the history
…33895)

Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent 76f6239 commit 2e4af86
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 16 deletions.
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

0 comments on commit 2e4af86

Please sign in to comment.