Skip to content

Commit 83642cb

Browse files
fix: Visitor "lead capture" failing when there was no email/phone already registered (RocketChat#36835)
Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com>
1 parent 6a5eb79 commit 83642cb

5 files changed

Lines changed: 318 additions & 15 deletions

File tree

.changeset/fair-dolls-trade.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/models': patch
3+
'@rocket.chat/meteor': patch
4+
---
5+
6+
Fixes the capture of lead's email or phone number when the visitor didn't have data already

apps/meteor/app/livechat/server/hooks/leadCapture.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,28 @@ callbacks.add(
3737
return message;
3838
}
3939

40-
const phoneRegexp = new RegExp(settings.get<string>('Livechat_lead_phone_regex'), 'g');
41-
const msgPhones = message.msg.match(phoneRegexp)?.filter(isTruthy) || [];
40+
const phoneRegexSetting = settings.get<string>('Livechat_lead_phone_regex');
41+
const emailRegexSetting = settings.get<string>('Livechat_lead_email_regex');
4242

43-
const emailRegexp = new RegExp(settings.get<string>('Livechat_lead_email_regex'), 'gi');
44-
const msgEmails = message.msg.match(emailRegexp)?.filter(isTruthy) || [];
45-
if (msgEmails || msgPhones) {
46-
await LivechatVisitors.saveGuestEmailPhoneById(room.v._id, msgEmails, msgPhones);
43+
const safeMatch = (pattern: string, flags: string, text: string): string[] => {
44+
if (!pattern) {
45+
return [];
46+
}
47+
try {
48+
const re = new RegExp(pattern, flags);
49+
return text.match(re)?.filter(isTruthy) ?? [];
50+
} catch {
51+
return [];
52+
}
53+
};
4754

55+
const uniq = (arr: string[]) => [...new Set(arr.filter(isTruthy))];
56+
57+
const matchedPhones = uniq(safeMatch(phoneRegexSetting, 'g', message.msg));
58+
const matchedEmails = uniq(safeMatch(emailRegexSetting, 'gi', message.msg));
59+
60+
if (matchedEmails.length || matchedPhones.length) {
61+
await LivechatVisitors.saveGuestEmailPhoneById(room.v._id, matchedEmails, matchedPhones);
4862
await callbacks.run('livechat.leadCapture', room);
4963
}
5064

apps/meteor/tests/data/livechat/rooms.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,55 @@ export const createLivechatRoomWidget = async (
5555
return response.body.room;
5656
};
5757

58+
export const createVisitorWithCustomData = async ({
59+
department,
60+
visitorName,
61+
customPhone,
62+
customFields,
63+
customToken,
64+
customEmail,
65+
ignoreEmail = false,
66+
ignorePhone = false,
67+
}: {
68+
department?: string;
69+
customPhone?: string;
70+
visitorName?: string;
71+
customEmail?: string;
72+
customFields?: { key: string; value: string; overwrite: boolean }[];
73+
customToken?: string;
74+
ignoreEmail?: boolean;
75+
ignorePhone?: boolean;
76+
}): Promise<ILivechatVisitor> => {
77+
const token = customToken || getRandomVisitorToken();
78+
const email = customEmail || `${token}@${token}.com`;
79+
const phone = customPhone || `${Math.floor(Math.random() * 10000000000)}`;
80+
81+
try {
82+
const res = await request.get(api(`livechat/visitor/${token}`));
83+
if (res?.body?.visitor) {
84+
return res.body.visitor as ILivechatVisitor;
85+
}
86+
} catch {
87+
// Ignore errors from GET; we will create the visitor below.
88+
}
89+
90+
const res = await request
91+
.post(api('livechat/visitor'))
92+
.set(credentials)
93+
.send({
94+
visitor: {
95+
name: visitorName || `Visitor ${Date.now()}`,
96+
token,
97+
customFields: customFields || [{ key: 'address', value: 'Rocket.Chat street', overwrite: true }],
98+
...(department ? { department } : {}),
99+
...(!ignoreEmail ? { email } : {}),
100+
...(!ignorePhone ? { phone } : {}),
101+
},
102+
});
103+
104+
return res.body.visitor as ILivechatVisitor;
105+
};
106+
58107
export const createVisitor = (
59108
department?: string,
60109
visitorName?: string,

apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {
1313
createVisitor,
1414
startANewLivechatRoomAndTakeIt,
1515
closeOmnichannelRoom,
16+
createVisitorWithCustomData,
17+
sendAgentMessage,
18+
sendMessage,
1619
} from '../../../data/livechat/rooms';
1720
import { getRandomVisitorToken } from '../../../data/livechat/users';
1821
import { getLivechatVisitorByToken } from '../../../data/livechat/visitor';
@@ -1312,6 +1315,7 @@ describe('LIVECHAT - visitors', () => {
13121315
expect(res.body.visitors[0]).to.have.property('visitorEmails');
13131316
});
13141317
});
1318+
13151319
describe('omnichannel/contact', () => {
13161320
let contact: ILivechatVisitor;
13171321
it('should fail if user doesnt have view-l-room permission', async () => {
@@ -1433,4 +1437,232 @@ describe('LIVECHAT - visitors', () => {
14331437
expect(contact.livechatData).to.have.property(cfName, 'test');
14341438
});
14351439
});
1440+
1441+
describe('lead capture', () => {
1442+
let visitor: ILivechatVisitor;
1443+
let room: IOmnichannelRoom;
1444+
1445+
before(async () => {
1446+
visitor = await createVisitorWithCustomData({
1447+
ignoreEmail: true,
1448+
ignorePhone: true,
1449+
});
1450+
room = await createLivechatRoom(visitor.token);
1451+
await createAgent();
1452+
});
1453+
after(async () => {
1454+
await closeOmnichannelRoom(room._id);
1455+
});
1456+
1457+
it('should capture the data matching the email regex and add it to the visitor', async () => {
1458+
await sendMessage(room._id, 'My email is random@email.com', visitor.token);
1459+
1460+
visitor = await getLivechatVisitorByToken(visitor.token);
1461+
expect(visitor.visitorEmails).to.be.an('array');
1462+
expect(visitor.visitorEmails).to.have.lengthOf(1);
1463+
expect(visitor.visitorEmails?.[0].address).to.be.equal('random@email.com');
1464+
});
1465+
1466+
it('should capture more emails when the visitor sends the message', async () => {
1467+
await sendMessage(room._id, 'Another email is test@teste.com', visitor.token);
1468+
1469+
visitor = await getLivechatVisitorByToken(visitor.token);
1470+
expect(visitor.visitorEmails).to.be.an('array');
1471+
expect(visitor.visitorEmails).to.have.lengthOf(2);
1472+
const emails = visitor.visitorEmails?.map((e) => e.address);
1473+
expect(emails).to.include('test@teste.com');
1474+
});
1475+
1476+
it('should capture multiple emails', async () => {
1477+
await sendMessage(room._id, 'My emails are test@123.com notest@1234.com', visitor.token);
1478+
1479+
visitor = await getLivechatVisitorByToken(visitor.token);
1480+
expect(visitor.visitorEmails).to.be.an('array');
1481+
expect(visitor.visitorEmails).to.have.lengthOf(4);
1482+
const emails = visitor.visitorEmails?.map((e) => e.address);
1483+
expect(emails).to.include('test@123.com');
1484+
expect(emails).to.include('notest@1234.com');
1485+
});
1486+
1487+
it('should not add an email thats already registered', async () => {
1488+
await sendMessage(room._id, 'My email is test@123.com', visitor.token);
1489+
1490+
visitor = await getLivechatVisitorByToken(visitor.token);
1491+
expect(visitor.visitorEmails).to.be.an('array');
1492+
expect(visitor.visitorEmails).to.have.lengthOf(4);
1493+
});
1494+
1495+
it('should not save emails the agent sends', async () => {
1496+
await sendAgentMessage(room._id, 'Confirming your email is test@12345.com?', credentials);
1497+
1498+
visitor = await getLivechatVisitorByToken(visitor.token);
1499+
expect(visitor.visitorEmails).to.be.an('array');
1500+
expect(visitor.visitorEmails).to.have.lengthOf(4);
1501+
});
1502+
1503+
it('should save phone numbers matching the regex', async () => {
1504+
await sendMessage(room._id, 'My phone number is 12345678', visitor.token);
1505+
1506+
visitor = await getLivechatVisitorByToken(visitor.token);
1507+
expect(visitor.phone).to.be.an('array');
1508+
expect(visitor.phone).to.have.lengthOf(1);
1509+
expect(visitor.phone?.[0].phoneNumber).to.be.equal('12345678');
1510+
});
1511+
1512+
it('should capture more phone numbers when the visitor sends the message', async () => {
1513+
await sendMessage(room._id, 'Another phone number is 87654321 87654323', visitor.token);
1514+
1515+
visitor = await getLivechatVisitorByToken(visitor.token);
1516+
expect(visitor.phone).to.be.an('array');
1517+
expect(visitor.phone).to.have.lengthOf(3);
1518+
const phones = visitor.phone?.map((p) => p.phoneNumber);
1519+
expect(phones).to.include('87654321');
1520+
expect(phones).to.include('87654323');
1521+
});
1522+
1523+
it('should not add a phone number thats already registered', async () => {
1524+
await sendMessage(room._id, 'My phone number is 12345678', visitor.token);
1525+
1526+
visitor = await getLivechatVisitorByToken(visitor.token);
1527+
expect(visitor.phone).to.be.an('array');
1528+
expect(visitor.phone).to.have.lengthOf(3);
1529+
});
1530+
1531+
it('should not save phone numbers the agent sends', async () => {
1532+
await sendAgentMessage(room._id, 'Confirming your phone number is 99999999?', credentials);
1533+
visitor = await getLivechatVisitorByToken(visitor.token);
1534+
expect(visitor.phone).to.be.an('array');
1535+
expect(visitor.phone).to.have.lengthOf(3);
1536+
});
1537+
1538+
it('should capture both phones & emails when sent on the same message', async () => {
1539+
await sendMessage(room._id, 'My email is zardw@asdf.com and my phone is 11223344', visitor.token);
1540+
1541+
visitor = await getLivechatVisitorByToken(visitor.token);
1542+
expect(visitor.visitorEmails).to.be.an('array');
1543+
expect(visitor.visitorEmails).to.have.lengthOf(5);
1544+
const emails = visitor.visitorEmails?.map((e) => e.address);
1545+
expect(emails).to.include('zardw@asdf.com');
1546+
1547+
expect(visitor.phone).to.be.an('array');
1548+
expect(visitor.phone).to.have.lengthOf(4);
1549+
const phones = visitor.phone?.map((p) => p.phoneNumber);
1550+
expect(phones).to.include('11223344');
1551+
});
1552+
1553+
describe('when settings are empty', () => {
1554+
before(async () => {
1555+
await updateSetting('Livechat_lead_phone_regex', '');
1556+
await updateSetting('Livechat_lead_email_regex', '');
1557+
});
1558+
1559+
after(async () => {
1560+
// reset settings
1561+
await updateSetting('Livechat_lead_email_regex', '\\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\\.)+[A-Z]{2,4}\\b');
1562+
await updateSetting(
1563+
'Livechat_lead_phone_regex',
1564+
'((?:\\([0-9]{1,3}\\)|[0-9]{2})[ \\-]*?[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$)|[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$))',
1565+
);
1566+
});
1567+
1568+
it('should not capture any email or phone no matter what the visitor sends', async () => {
1569+
await sendMessage(room._id, 'Now my email is this@shalnotpass.com', visitor.token);
1570+
1571+
await sendMessage(room._id, 'And my phone number is 55667788', visitor.token);
1572+
1573+
visitor = await getLivechatVisitorByToken(visitor.token);
1574+
const emails = visitor.visitorEmails?.map((e) => e.address);
1575+
expect(emails || []).to.not.include('this@shalnotpass.com');
1576+
1577+
const phones = visitor.phone?.map((p) => p.phoneNumber);
1578+
expect(phones || []).to.not.include('55667788');
1579+
});
1580+
});
1581+
1582+
describe('when phone regex is broken', () => {
1583+
before(async () => {
1584+
await updateSetting('Livechat_lead_phone_regex', '(');
1585+
});
1586+
1587+
after(async () => {
1588+
// reset settings
1589+
await updateSetting(
1590+
'Livechat_lead_phone_regex',
1591+
'((?:\\([0-9]{1,3}\\)|[0-9]{2})[ \\-]*?[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$)|[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$))',
1592+
);
1593+
});
1594+
1595+
it('should capture email', async () => {
1596+
await sendMessage(room._id, 'Now my email is this@isok.com', visitor.token);
1597+
1598+
await sendMessage(room._id, 'And my phone number is 55667799', visitor.token);
1599+
1600+
visitor = await getLivechatVisitorByToken(visitor.token);
1601+
1602+
expect(visitor.visitorEmails).to.be.an('array');
1603+
expect(visitor.visitorEmails).to.have.lengthOf(6);
1604+
const emails = visitor.visitorEmails?.map((e) => e.address);
1605+
expect(emails).to.include('this@isok.com');
1606+
1607+
expect(visitor.phone).to.have.lengthOf(4);
1608+
const phones = visitor.phone?.map((p) => p.phoneNumber);
1609+
expect(phones || []).to.not.include('55667788');
1610+
});
1611+
});
1612+
1613+
describe('when email regex is broken', () => {
1614+
before(async () => {
1615+
await updateSetting('Livechat_lead_email_regex', '(');
1616+
});
1617+
1618+
after(async () => {
1619+
// reset settings
1620+
await updateSetting('Livechat_lead_email_regex', '\\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\\.)+[A-Z]{2,4}\\b');
1621+
});
1622+
1623+
it('should capture email', async () => {
1624+
await sendMessage(room._id, 'Now my email is this@shalnotpass.com', visitor.token);
1625+
1626+
await sendMessage(room._id, 'And my phone number is 98765432', visitor.token);
1627+
1628+
visitor = await getLivechatVisitorByToken(visitor.token);
1629+
1630+
expect(visitor.visitorEmails).to.have.lengthOf(6);
1631+
const emails = visitor.visitorEmails?.map((e) => e.address);
1632+
expect(emails || []).to.not.include('this@shalnotpass.com');
1633+
1634+
expect(visitor.phone).to.be.an('array');
1635+
expect(visitor.phone).to.have.lengthOf(5);
1636+
const phones = visitor.phone?.map((p) => p.phoneNumber);
1637+
expect(phones).to.include('98765432');
1638+
});
1639+
});
1640+
1641+
describe('when the visitor has emails & phones already', () => {
1642+
let newVisitor: ILivechatVisitor;
1643+
let newRoom: IOmnichannelRoom;
1644+
before(async () => {
1645+
newVisitor = await createVisitor();
1646+
newRoom = await createLivechatRoom(newVisitor.token);
1647+
});
1648+
after(async () => {
1649+
await closeOmnichannelRoom(newRoom._id);
1650+
});
1651+
1652+
it('should capture new emails & phones and add them to the existing ones', async () => {
1653+
await sendMessage(newRoom._id, 'My email is 1234@12344.com and my phone is 11223344', newVisitor.token);
1654+
1655+
newVisitor = await getLivechatVisitorByToken(newVisitor.token);
1656+
expect(newVisitor.visitorEmails).to.be.an('array');
1657+
expect(newVisitor.visitorEmails).to.have.lengthOf(2);
1658+
const emails = newVisitor.visitorEmails?.map((e) => e.address);
1659+
expect(emails).to.include('1234@12344.com');
1660+
1661+
expect(newVisitor.phone).to.be.an('array');
1662+
expect(newVisitor.phone).to.have.lengthOf(2);
1663+
const phones = newVisitor.phone?.map((p) => p.phoneNumber);
1664+
expect(phones).to.include('11223344');
1665+
});
1666+
});
1667+
});
14361668
});

packages/models/src/models/LivechatVisitors.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -380,18 +380,20 @@ export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> implements IL
380380
.filter((phone) => phone?.trim().replace(/[^\d]/g, ''))
381381
.map((phone) => ({ phoneNumber: phone }));
382382

383-
const update: UpdateFilter<ILivechatVisitor> = {
384-
$addToSet: {
385-
...(saveEmail.length && { visitorEmails: { $each: saveEmail } }),
386-
...(savePhone.length && { phone: { $each: savePhone } }),
387-
},
388-
};
389-
390-
if (!Object.keys(update.$addToSet as Record<string, any>).length) {
383+
if (!saveEmail.length && !savePhone.length) {
391384
return Promise.resolve();
392385
}
393386

394-
return this.updateOne({ _id }, update);
387+
// the only reason we're using $setUnion here instead of $addToSet is because
388+
// old visitors might have `visitorEmails` or `phone` as `null` which would cause $addToSet to fail
389+
return this.updateOne({ _id }, [
390+
{
391+
$set: {
392+
...(saveEmail.length && { visitorEmails: { $setUnion: [{ $ifNull: ['$visitorEmails', []] }, saveEmail] } }),
393+
...(savePhone.length && { phone: { $setUnion: [{ $ifNull: ['$phone', []] }, savePhone] } }),
394+
},
395+
},
396+
]);
395397
}
396398

397399
removeContactManagerByUsername(manager: string): Promise<Document | UpdateResult> {

0 commit comments

Comments
 (0)