From 1df1b1f8e41e3d32a2d96d0777c508f276ec48a5 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 17 Jan 2023 10:39:17 -0800 Subject: [PATCH 01/77] fix: Update the inbox id if changed (#6272) --- .../routes/dashboard/settings/campaigns/EditCampaign.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue b/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue index 6ad996319..af17dc079 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/campaigns/EditCampaign.vue @@ -259,7 +259,7 @@ export default { id: this.selectedCampaign.id, title: this.title, message: this.message, - inbox_id: this.$route.params.inboxId, + inbox_id: this.selectedInbox, trigger_only_during_business_hours: // eslint-disable-next-line prettier/prettier this.triggerOnlyDuringBusinessHours, From 37b9816827b9c3e046aff97ccb01f173650093d0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 18 Jan 2023 11:23:40 +0530 Subject: [PATCH 02/77] feat: more events tracking for SaaS (#6234) --- .../components/widgets/WootWriter/Editor.vue | 8 +- .../widgets/conversation/ReplyBox.vue | 6 +- .../helper/AnalyticsHelper/events.js | 82 +++++++++-- .../dashboard/helper/AnalyticsHelper/index.js | 35 ++++- .../helper/AnalyticsHelper/plugin.js | 11 ++ .../AnalyticsHelper/specs/events.spec.js | 26 ++++ .../AnalyticsHelper/specs/helper.spec.js | 139 ++++++++++++++++++ .../AnalyticsHelper/specs/plugin.spec.js | 35 +++++ .../modules/contact/ContactMergeModal.vue | 6 +- .../components/MessageContextMenu.vue | 6 +- .../components/ContactsAdvancedFilters.vue | 7 + .../contacts/components/ContactsView.vue | 9 ++ .../contacts/components/ImportContacts.vue | 6 + .../conversation/Macros/MacroItem.vue | 7 +- .../dashboard/customviews/AddCustomViews.vue | 5 + .../customviews/DeleteCustomViews.vue | 4 + .../helpcenter/components/AddLocale.vue | 6 + .../components/Header/EditArticleHeader.vue | 6 + .../helpcenter/components/PortalListItem.vue | 9 ++ .../helpcenter/pages/articles/EditArticle.vue | 8 + .../helpcenter/pages/articles/NewArticle.vue | 4 + .../pages/categories/AddCategory.vue | 4 + .../pages/categories/CategoryListItem.vue | 6 +- .../pages/categories/EditCategory.vue | 2 + .../pages/categories/ListAllCategories.vue | 8 +- .../pages/portals/EditPortalLocales.vue | 9 ++ .../pages/portals/PortalCustomization.vue | 8 +- .../pages/portals/PortalDetails.vue | 6 + .../settings/campaigns/AddCampaign.vue | 12 ++ .../settings/reports/CsatResponses.vue | 12 ++ .../dashboard/settings/reports/Index.vue | 20 +++ .../reports/components/WootReports.vue | 21 +++ .../dashboard/store/modules/campaigns.js | 4 + .../store/modules/contacts/actions.js | 4 + .../store/modules/conversations/actions.js | 9 +- .../dashboard/store/modules/csat.js | 5 + .../dashboard/store/modules/inboxes.js | 7 +- .../dashboard/store/modules/labels.js | 5 + .../dashboard/store/modules/reports.js | 18 +++ app/javascript/packs/application.js | 4 +- 40 files changed, 539 insertions(+), 50 deletions(-) create mode 100644 app/javascript/dashboard/helper/AnalyticsHelper/plugin.js create mode 100644 app/javascript/dashboard/helper/AnalyticsHelper/specs/events.spec.js create mode 100644 app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js create mode 100644 app/javascript/dashboard/helper/AnalyticsHelper/specs/plugin.spec.js diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index ef3a58979..6093bf6b0 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -43,9 +43,7 @@ import { import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../helper/AnalyticsHelper'; +import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; const createState = (content, placeholder, plugins = []) => { return EditorState.create({ @@ -265,7 +263,7 @@ export default { ); this.state = this.editorView.state.apply(tr); this.emitOnChange(); - AnalyticsHelper.track(ANALYTICS_EVENTS.USED_MENTIONS); + this.$track(CONVERSATION_EVENTS.USED_MENTIONS); return false; }, @@ -295,7 +293,7 @@ export default { this.emitOnChange(); tr.scrollIntoView(); - AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE); + this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); return false; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index fe25419a9..56937b3ec 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -164,9 +164,7 @@ import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage'; import { trimContent, debounce } from '@chatwoot/utils'; import wootConstants from 'dashboard/constants'; import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../helper/AnalyticsHelper'; +import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; const EmojiInput = () => import('shared/components/emoji/EmojiInput'); @@ -710,7 +708,7 @@ export default { }, replaceText(message) { setTimeout(() => { - AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE); + this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); this.message = message; }, 100); }, diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/events.js b/app/javascript/dashboard/helper/AnalyticsHelper/events.js index b0fa7ee1a..3d8b0f901 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/events.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/events.js @@ -1,9 +1,73 @@ -export const EXECUTED_A_MACRO = 'Executed a macro'; -export const SENT_MESSAGE = 'Sent a message'; -export const SENT_PRIVATE_NOTE = 'Sent a private note'; -export const INSERTED_A_CANNED_RESPONSE = 'Inserted a canned response'; -export const USED_MENTIONS = 'Used mentions'; -export const MERGED_CONTACTS = 'Used merge contact option'; -export const ADDED_TO_CANNED_RESPONSE = 'Used added to canned response option'; -export const ADDED_A_CUSTOM_ATTRIBUTE = 'Added a custom attribute'; -export const ADDED_AN_INBOX = 'Added an inbox'; +export const CONVERSATION_EVENTS = Object.freeze({ + EXECUTED_A_MACRO: 'Executed a macro', + SENT_MESSAGE: 'Sent a message', + SENT_PRIVATE_NOTE: 'Sent a private note', + INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response', + USED_MENTIONS: 'Used mentions', +}); + +export const ACCOUNT_EVENTS = Object.freeze({ + ADDED_TO_CANNED_RESPONSE: 'Used added to canned response option', + ADDED_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute', + ADDED_AN_INBOX: 'Added an inbox', +}); + +export const LABEL_EVENTS = Object.freeze({ + CREATE: 'Created a label', + UPDATE: 'Updated a label', + DELETED: 'Deleted a label', + APPLY_LABEL: 'Applied a label', +}); + +// REPORTS EVENTS +export const REPORTS_EVENTS = Object.freeze({ + DOWNLOAD_REPORT: 'Downloaded a report', + FILTER_REPORT: 'Used filters in the reports', +}); + +// CONTACTS PAGE EVENTS +export const CONTACTS_EVENTS = Object.freeze({ + APPLY_FILTER: 'Applied filters in the contacts list', + SAVE_FILTER: 'Saved a filter in the contacts list', + DELETE_FILTER: 'Deleted a filter in the contacts list', + + APPLY_SORT: 'Sorted contacts list', + SEARCH: 'Searched contacts list', + CREATE_CONTACT: 'Created a contact', + MERGED_CONTACTS: 'Used merge contact option', + IMPORT_MODAL_OPEN: 'Opened import contacts modal', + IMPORT_FAILURE: 'Import contacts failed', + IMPORT_SUCCESS: 'Imported contacts successfully', +}); + +// CAMPAIGN EVENTS +export const CAMPAIGNS_EVENTS = Object.freeze({ + OPEN_NEW_CAMPAIGN_MODAL: 'Opened new campaign modal', + CREATE_CAMPAIGN: 'Created a new campaign', + UPDATE_CAMPAIGN: 'Updated a campaign', + DELETE_CAMPAIGN: 'Deleted a campaign', +}); + +// PORTAL EVENTS +export const PORTALS_EVENTS = Object.freeze({ + ONBOARD_BASIC_INFORMATION: 'New Portal: Completed basic information', + ONBOARD_CUSTOMIZATION: 'New portal: Completed customization', + CREATE_PORTAL: 'Created a portal', + DELETE_PORTAL: 'Deleted a portal', + UPDATE_PORTAL: 'Updated a portal', + + CREATE_LOCALE: 'Created a portal locale', + SET_DEFAULT_LOCALE: 'Set default portal locale', + DELETE_LOCALE: 'Deleted a portal locale', + SWITCH_LOCALE: 'Switched portal locale', + + CREATE_CATEGORY: 'Created a portal category', + DELETE_CATEGORY: 'Deleted a portal category', + EDIT_CATEGORY: 'Edited a portal category', + + CREATE_ARTICLE: 'Created an article', + PUBLISH_ARTICLE: 'Published an article', + ARCHIVE_ARTICLE: 'Archived an article', + DELETE_ARTICLE: 'Deleted an article', + PREVIEW_ARTICLE: 'Previewed article', +}); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/index.js b/app/javascript/dashboard/helper/AnalyticsHelper/index.js index e082182cf..bf91d9a91 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/index.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/index.js @@ -1,12 +1,26 @@ import { AnalyticsBrowser } from '@june-so/analytics-next'; -class AnalyticsHelper { +/** + * AnalyticsHelper class to initialize and track user analytics + * @class AnalyticsHelper + */ +export class AnalyticsHelper { + /** + * @constructor + * @param {Object} [options={}] - options for analytics + * @param {string} [options.token] - analytics token + */ constructor({ token: analyticsToken } = {}) { this.analyticsToken = analyticsToken; this.analytics = null; this.user = {}; } + /** + * Initialize analytics + * @function + * @async + */ async init() { if (!this.analyticsToken) { return; @@ -18,6 +32,11 @@ class AnalyticsHelper { this.analytics = analytics; } + /** + * Identify the user + * @function + * @param {Object} user - User object + */ identify(user) { if (!this.analytics) { return; @@ -41,6 +60,12 @@ class AnalyticsHelper { } } + /** + * Track any event + * @function + * @param {string} eventName - event name + * @param {Object} [properties={}] - event properties + */ track(eventName, properties = {}) { if (!this.analytics) { return; @@ -53,6 +78,11 @@ class AnalyticsHelper { }); } + /** + * Track the page views + * @function + * @param {Object} params - Page view properties + */ page(params) { if (!this.analytics) { return; @@ -62,6 +92,5 @@ class AnalyticsHelper { } } -export * as ANALYTICS_EVENTS from './events'; - +// This object is shared across, the init is called in app/javascript/packs/application.js export default new AnalyticsHelper(window.analyticsConfig); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/plugin.js b/app/javascript/dashboard/helper/AnalyticsHelper/plugin.js new file mode 100644 index 000000000..5ed12f1f5 --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/plugin.js @@ -0,0 +1,11 @@ +import analyticsHelper from '.'; + +export default { + // This function is called when the Vue plugin is installed + install(Vue) { + analyticsHelper.init(); + Vue.prototype.$analytics = analyticsHelper; + // Add a shorthand function for the track method on the helper module + Vue.prototype.$track = analyticsHelper.track.bind(analyticsHelper); + }, +}; diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/specs/events.spec.js b/app/javascript/dashboard/helper/AnalyticsHelper/specs/events.spec.js new file mode 100644 index 000000000..683b1c593 --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/specs/events.spec.js @@ -0,0 +1,26 @@ +import * as AnalyticsEvents from '../events'; + +describe('Analytics Events', () => { + it('should be frozen', () => { + Object.entries(AnalyticsEvents).forEach(([, value]) => { + expect(Object.isFrozen(value)).toBe(true); + }); + }); + + it('event names should be unique across the board', () => { + const allValues = Object.values(AnalyticsEvents).reduce( + (acc, curr) => acc.concat(Object.values(curr)), + [] + ); + const uniqueValues = new Set(allValues); + expect(allValues.length).toBe(uniqueValues.size); + }); + + it('should not allow properties to be modified', () => { + Object.values(AnalyticsEvents).forEach(eventsObject => { + expect(() => { + eventsObject.NEW_PROPERTY = 'new value'; + }).toThrow(); + }); + }); +}); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js b/app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js new file mode 100644 index 000000000..ef02ac201 --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js @@ -0,0 +1,139 @@ +import helperObject, { AnalyticsHelper } from '../'; + +jest.mock('@june-so/analytics-next', () => ({ + AnalyticsBrowser: { + load: () => [ + { + identify: jest.fn(), + track: jest.fn(), + page: jest.fn(), + group: jest.fn(), + }, + ], + }, +})); + +describe('helperObject', () => { + it('should return an instance of AnalyticsHelper', () => { + expect(helperObject).toBeInstanceOf(AnalyticsHelper); + }); +}); + +describe('AnalyticsHelper', () => { + let analyticsHelper; + beforeEach(() => { + analyticsHelper = new AnalyticsHelper({ token: 'test_token' }); + }); + + describe('init', () => { + it('should initialize the analytics browser with the correct token', async () => { + await analyticsHelper.init(); + expect(analyticsHelper.analytics).not.toBe(null); + }); + + it('should not initialize the analytics browser if token is not provided', async () => { + analyticsHelper = new AnalyticsHelper(); + await analyticsHelper.init(); + expect(analyticsHelper.analytics).toBe(null); + }); + }); + + describe('identify', () => { + beforeEach(() => { + analyticsHelper.analytics = { identify: jest.fn(), group: jest.fn() }; + }); + + it('should call identify on analytics browser with correct arguments', () => { + analyticsHelper.identify({ + id: '123', + email: 'test@example.com', + name: 'Test User', + avatar_url: 'avatar_url', + accounts: [{ id: '1', name: 'Account 1' }], + account_id: '1', + }); + + expect(analyticsHelper.analytics.identify).toHaveBeenCalledWith( + 'test@example.com', + { + userId: '123', + email: 'test@example.com', + name: 'Test User', + avatar: 'avatar_url', + } + ); + expect(analyticsHelper.analytics.group).toHaveBeenCalled(); + }); + + it('should call identify on analytics browser without group', () => { + analyticsHelper.identify({ + id: '123', + email: 'test@example.com', + name: 'Test User', + avatar_url: 'avatar_url', + accounts: [{ id: '1', name: 'Account 1' }], + account_id: '5', + }); + + expect(analyticsHelper.analytics.group).not.toHaveBeenCalled(); + }); + + it('should not call analytics.page if analytics is null', () => { + analyticsHelper.analytics = null; + analyticsHelper.identify({}); + expect(analyticsHelper.analytics).toBe(null); + }); + }); + + describe('track', () => { + beforeEach(() => { + analyticsHelper.analytics = { track: jest.fn() }; + analyticsHelper.user = { id: '123' }; + }); + + it('should call track on analytics browser with correct arguments', () => { + analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' }); + expect(analyticsHelper.analytics.track).toHaveBeenCalledWith({ + userId: '123', + event: 'Test Event', + properties: { prop1: 'value1', prop2: 'value2' }, + }); + }); + + it('should call track on analytics browser with default properties', () => { + analyticsHelper.track('Test Event'); + expect(analyticsHelper.analytics.track).toHaveBeenCalledWith({ + userId: '123', + event: 'Test Event', + properties: {}, + }); + }); + + it('should not call track on analytics browser if analytics is not initialized', () => { + analyticsHelper.analytics = null; + analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' }); + expect(analyticsHelper.analytics).toBe(null); + }); + }); + + describe('page', () => { + beforeEach(() => { + analyticsHelper.analytics = { page: jest.fn() }; + }); + + it('should call the analytics.page method with the correct arguments', () => { + const params = { + name: 'Test page', + url: '/test', + }; + analyticsHelper.page(params); + expect(analyticsHelper.analytics.page).toHaveBeenCalledWith(params); + }); + + it('should not call analytics.page if analytics is null', () => { + analyticsHelper.analytics = null; + analyticsHelper.page(); + expect(analyticsHelper.analytics).toBe(null); + }); + }); +}); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/specs/plugin.spec.js b/app/javascript/dashboard/helper/AnalyticsHelper/specs/plugin.spec.js new file mode 100644 index 000000000..407253f4b --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/specs/plugin.spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import plugin from '../plugin'; +import analyticsHelper from '../index'; + +describe('Vue Analytics Plugin', () => { + beforeEach(() => { + jest.spyOn(analyticsHelper, 'init'); + jest.spyOn(analyticsHelper, 'track'); + Vue.use(plugin); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + it('should call the init method on the analyticsHelper', () => { + expect(analyticsHelper.init).toHaveBeenCalled(); + }); + + it('should add the analyticsHelper to the Vue prototype', () => { + expect(Vue.prototype.$analytics).toBe(analyticsHelper); + }); + + it('should add the track method to the Vue prototype', () => { + expect(typeof Vue.prototype.$track).toBe('function'); + Vue.prototype.$track('eventName'); + expect(analyticsHelper.track).toHaveBeenCalledWith('eventName'); + }); + + it('should call the track method on the analyticsHelper when $track is called', () => { + Vue.prototype.$track('eventName'); + expect(analyticsHelper.track).toHaveBeenCalledWith('eventName'); + }); +}); diff --git a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue index 62f8b001a..e33dcbcf4 100644 --- a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue +++ b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue @@ -24,9 +24,7 @@ import MergeContact from 'dashboard/modules/contact/components/MergeContact'; import ContactAPI from 'dashboard/api/contacts'; import { mapGetters } from 'vuex'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../helper/AnalyticsHelper'; +import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events'; export default { components: { MergeContact }, @@ -75,7 +73,7 @@ export default { } }, async onMergeContacts(childContactId) { - AnalyticsHelper.track(ANALYTICS_EVENTS.MERGED_CONTACTS); + this.$track(CONTACTS_EVENTS.MERGED_CONTACTS); try { await this.$store.dispatch('contacts/merge', { childId: childContactId, diff --git a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue index 17fb5ab7e..7070a53b4 100644 --- a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue +++ b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue @@ -72,9 +72,7 @@ import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; import { copyTextToClipboard } from 'shared/helpers/clipboard'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../helper/AnalyticsHelper'; +import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events'; export default { components: { @@ -130,7 +128,7 @@ export default { this.$emit('toggle', false); }, showCannedResponseModal() { - AnalyticsHelper.track(ANALYTICS_EVENTS.ADDED_TO_CANNED_RESPONSE); + this.$track(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE); this.isCannedResponseModalOpen = true; }, }, diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue index 95c9ae61c..3e988e0a4 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue @@ -70,6 +70,7 @@ import { mapGetters } from 'vuex'; import { filterAttributeGroups } from '../contactFilterItems'; import filterMixin from 'shared/mixins/filterMixin'; import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; +import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; export default { components: { FilterInputBox, @@ -251,6 +252,12 @@ export default { JSON.parse(JSON.stringify(this.appliedFilters)) ); this.$emit('applyFilter', this.appliedFilters); + this.$track(CONTACTS_EVENTS.APPLY_FILTER, { + applied_filters: this.appliedFilters.map(filter => ({ + key: filter.attribute_key, + operator: filter.filter_operator, + })), + }); }, resetFilter(index, currentFilter) { this.appliedFilters[index].filter_operator = this.filterTypes.find( diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index e09c4a2d8..afbcfd5a5 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -86,6 +86,7 @@ import contactFilterItems from '../contactFilterItems'; import filterQueryGenerator from '../../../../helper/filterQueryGenerator'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews'; +import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; const DEFAULT_PAGE = 1; const FILTER_TYPE_CONTACT = 1; @@ -334,6 +335,14 @@ export default { onSortChange(params) { this.sortConfig = params; this.fetchContacts(this.meta.currentPage); + + const sortBy = + Object.entries(params).find(pair => Boolean(pair[1])) || []; + + this.$track(CONTACTS_EVENTS.APPLY_SORT, { + appliedOn: sortBy[0], + order: sortBy[1], + }); }, onToggleFilters() { this.showFiltersModal = !this.showFiltersModal; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue index a8e582bcf..91393d334 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue @@ -45,6 +45,7 @@ import Modal from '../../../../components/Modal'; import { mapGetters } from 'vuex'; import alertMixin from 'shared/mixins/alertMixin'; +import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; export default { components: { @@ -71,6 +72,9 @@ export default { return '/downloads/import-contacts-sample.csv'; }, }, + mounted() { + this.$track(CONTACTS_EVENTS.IMPORT_MODAL_OPEN); + }, methods: { async uploadFile() { try { @@ -78,10 +82,12 @@ export default { await this.$store.dispatch('contacts/import', this.file); this.onClose(); this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE')); + this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS); } catch (error) { this.showAlert( error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE') ); + this.$track(CONTACTS_EVENTS.IMPORT_FAILURE); } }, handleFileUpload() { diff --git a/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue index e013af14c..f683690d4 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue @@ -35,9 +35,8 @@ import alertMixin from 'shared/mixins/alertMixin'; import { mixin as clickaway } from 'vue-clickaway'; import MacroPreview from './MacroPreview'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../../helper/AnalyticsHelper'; +import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events'; + export default { components: { MacroPreview, @@ -67,7 +66,7 @@ export default { macroId: macro.id, conversationIds: [this.conversationId], }); - AnalyticsHelper.track(ANALYTICS_EVENTS.EXECUTED_A_MACRO); + this.$track(CONVERSATION_EVENTS.EXECUTED_A_MACRO); this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY')); } catch (error) { this.showAlert(this.$t('MACROS.ERROR')); diff --git a/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue b/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue index 37e532e02..c87c85883 100644 --- a/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue +++ b/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue @@ -31,6 +31,7 @@ + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue index 57498c5d8..0229f03b3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue @@ -90,6 +90,10 @@ +
@@ -109,12 +113,14 @@ import inboxMixin from 'shared/mixins/inboxMixin'; import SettingsSection from '../../../../../components/SettingsSection'; import ImapSettings from '../ImapSettings'; import SmtpSettings from '../SmtpSettings'; +import MicrosoftReauthorize from '../channels/microsoft/Reauthorize'; export default { components: { SettingsSection, ImapSettings, SmtpSettings, + MicrosoftReauthorize, }, mixins: [inboxMixin, alertMixin], props: { diff --git a/app/views/microsoft/identity_association.json.erb b/app/views/microsoft/identity_association.json.erb new file mode 100644 index 000000000..4cfacef02 --- /dev/null +++ b/app/views/microsoft/identity_association.json.erb @@ -0,0 +1,7 @@ +{ + "associatedApplications": [ + { + "applicationId": "<%= ENV['AZURE_APP_ID'] %>" + } + ] + } diff --git a/config/routes.rb b/config/routes.rb index 912324842..13cb9cedc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -350,6 +350,7 @@ Rails.application.routes.draw do # Routes for external service verifications get 'apple-app-site-association' => 'apple_app#site_association' get '.well-known/assetlinks.json' => 'android_app#assetlinks' + get '.well-known/microsoft-identity-association.json' => 'microsoft#identity_association' # ---------------------------------------------------------------------- # Internal Monitoring Routes diff --git a/spec/controllers/microsoft_controller_spec.rb b/spec/controllers/microsoft_controller_spec.rb new file mode 100644 index 000000000..6ce22d66b --- /dev/null +++ b/spec/controllers/microsoft_controller_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +describe '/.well-known/microsoft-identity-association.json', type: :request do + describe 'GET /.well-known/microsoft-identity-association.json' do + it 'successfully retrieves assetlinks.json file' do + with_modified_env AZURE_APP_ID: 'azure-application-client-id' do + get '/.well-known/microsoft-identity-association.json' + expect(response).to have_http_status(:success) + expect(response.body).to include '"applicationId": "azure-application-client-id"' + end + end + end +end From 6151e42bdf4b42c65178aa0e92e2c6a8953a3d8e Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Thu, 19 Jan 2023 13:40:50 +0530 Subject: [PATCH 05/77] Filter and return only approved templates (#6288) --- .../conversation/WhatsappTemplates/TemplatesPicker.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue index b9e9ba71c..b3130f72e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue +++ b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue @@ -67,7 +67,9 @@ export default { }, computed: { whatsAppTemplateMessages() { - return this.$store.getters['inboxes/getWhatsAppTemplates'](this.inboxId); + return this.$store.getters['inboxes/getWhatsAppTemplates']( + this.inboxId + ).filter(template => template.status === 'approved'); }, filteredTemplateMessages() { return this.whatsAppTemplateMessages.filter(template => From 905fca7869a4c6e2e67ab6f6008b20d42e47505c Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:40:46 +0530 Subject: [PATCH 06/77] fix: Whatsapp template picker bug - Enforce lowercasing the template status value before checking the value --- .../widgets/conversation/WhatsappTemplates/TemplatesPicker.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue index b3130f72e..7de46eaf5 100644 --- a/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue +++ b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue @@ -69,7 +69,7 @@ export default { whatsAppTemplateMessages() { return this.$store.getters['inboxes/getWhatsAppTemplates']( this.inboxId - ).filter(template => template.status === 'approved'); + ).filter(template => template.status.toLowerCase() === 'approved'); }, filteredTemplateMessages() { return this.whatsAppTemplateMessages.filter(template => From a86c2705e989c56cab325cedecb988eb8d8e24dc Mon Sep 17 00:00:00 2001 From: fgrep Date: Thu, 19 Jan 2023 08:49:27 -0300 Subject: [PATCH 07/77] Fix: more events tracking for SaaS (#6234) (#6298) --- .../dashboard/store/modules/conversations/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index bad195642..48fa4fa58 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -175,8 +175,8 @@ const actions = { const response = await MessageApi.create(pendingMessage); AnalyticsHelper.track( pendingMessage.private - ? CONVERSATION_EVENTS.CONVERSATION.SENT_PRIVATE_NOTE - : CONVERSATION_EVENTS.CONVERSATION.SENT_MESSAGE + ? CONVERSATION_EVENTS.SENT_PRIVATE_NOTE + : CONVERSATION_EVENTS.SENT_MESSAGE ); commit(types.ADD_MESSAGE, { ...response.data, From 1193cf1847ce09d11fad9a5d87dddc693268090b Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 19 Jan 2023 18:49:57 +0530 Subject: [PATCH 08/77] feat: ignore errors from extensions (#6297) This PR ignores errors from chrome and safari extensions, and any local scripts by developers --- app/javascript/packs/application.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 0ee8fd663..4fe005069 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -37,6 +37,19 @@ if (window.errorLoggingConfig) { Sentry.init({ Vue, dsn: window.errorLoggingConfig, + denyUrls: [ + // Chrome extensions + /^chrome:\/\//i, + /chrome-extension:/i, + /extensions\//i, + + // Locally saved copies + /file:\/\//i, + + // Safari extensions. + /safari-web-extension:/i, + /safari-extension:/i, + ], integrations: [new Integrations.BrowserTracing()], }); } From e2ccac78d27691e8c3ef2a69aea54e764ee6156a Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 19 Jan 2023 18:52:38 +0530 Subject: [PATCH 09/77] fix: Error when unsupported Whatsapp message status (#6295) fixes error when unsupported WhatsApp message status --- .../whatsapp/incoming_message_base_service.rb | 19 +++++-------------- .../incoming_message_service_helpers.rb | 14 ++++++++++++++ .../whatsapp/incoming_message_service_spec.rb | 11 +++++++++++ 3 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 app/services/whatsapp/incoming_message_service_helpers.rb diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb index 83fceb452..5dc495412 100644 --- a/app/services/whatsapp/incoming_message_base_service.rb +++ b/app/services/whatsapp/incoming_message_base_service.rb @@ -2,6 +2,8 @@ # https://docs.360dialog.com/whatsapp-api/whatsapp-api/media # https://developers.facebook.com/docs/whatsapp/api/media/ class Whatsapp::IncomingMessageBaseService + include ::Whatsapp::IncomingMessageServiceHelpers + pattr_initialize [:inbox!, :params!] def perform @@ -37,6 +39,8 @@ class Whatsapp::IncomingMessageBaseService return unless find_message_by_source_id(@processed_params[:statuses].first[:id]) update_message_with_status(@message, @processed_params[:statuses].first) + rescue ArgumentError => e + Rails.logger.error "Error while processing whatsapp status update #{e.message}" end def update_message_with_status(message, status) @@ -49,7 +53,7 @@ class Whatsapp::IncomingMessageBaseService end def create_messages - return if unprocessable_message_type? + return if unprocessable_message_type?(message_type) @message = @conversation.messages.build( content: message_content(@processed_params[:messages].first), @@ -108,23 +112,10 @@ class Whatsapp::IncomingMessageBaseService @conversation = ::Conversation.create!(conversation_params) end - def file_content_type(file_type) - return :image if %w[image sticker].include?(file_type) - return :audio if %w[audio voice].include?(file_type) - return :video if ['video'].include?(file_type) - return :location if ['location'].include?(file_type) - - :file - end - def message_type @processed_params[:messages].first[:type] end - def unprocessable_message_type? - %w[reaction contacts ephemeral unsupported].include?(message_type) - end - def attach_files return if %w[text button interactive location].include?(message_type) diff --git a/app/services/whatsapp/incoming_message_service_helpers.rb b/app/services/whatsapp/incoming_message_service_helpers.rb new file mode 100644 index 000000000..263e21c11 --- /dev/null +++ b/app/services/whatsapp/incoming_message_service_helpers.rb @@ -0,0 +1,14 @@ +module Whatsapp::IncomingMessageServiceHelpers + def file_content_type(file_type) + return :image if %w[image sticker].include?(file_type) + return :audio if %w[audio voice].include?(file_type) + return :video if ['video'].include?(file_type) + return :location if ['location'].include?(file_type) + + :file + end + + def unprocessable_message_type?(message_type) + %w[reaction contacts ephemeral unsupported].include?(message_type) + end +end diff --git a/spec/services/whatsapp/incoming_message_service_spec.rb b/spec/services/whatsapp/incoming_message_service_spec.rb index 3721d48cd..a6734f32d 100644 --- a/spec/services/whatsapp/incoming_message_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_service_spec.rb @@ -113,6 +113,17 @@ describe Whatsapp::IncomingMessageService do expect(message.reload.status).to eq('failed') expect(message.external_error).to eq('123: abc') end + + it 'will not throw error if unsupported status' do + status_params = { + 'statuses' => [{ 'recipient_id' => from, 'id' => from, 'status' => 'deleted', + 'errors' => [{ 'code': 123, 'title': 'abc' }] }] + }.with_indifferent_access + + message = Message.find_by!(source_id: from) + expect(message.status).to eq('sent') + expect { described_class.new(inbox: whatsapp_channel.inbox, params: status_params).perform }.not_to raise_error + end end context 'when valid interactive message params' do From 845311a539286376a07c05848d1539423cc7cfda Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 19 Jan 2023 18:53:21 +0530 Subject: [PATCH 10/77] chore: add stale PR bot (#6289) The PR only adds the stale label and puts a comment, does not close them (yet) --- .github/workflows/stale.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..7a7564ecb --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,28 @@ +# This workflow warns and then closes PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '28 3 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-issue-close: -1, + days-before-issue-stale: -1 + days-before-pr-close: -1, + days-before-pr-stale: 30, + stale-pr-message: '🐢 Turtley slow progress alert! This pull request has been idle for over 30 days. Can we please speed things up and either merge it or release it back into the wild?' + stale-pr-label: 'stale' From a74506847386bfc1eb04ebc121367c69458b8cd9 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 19 Jan 2023 18:53:42 +0530 Subject: [PATCH 11/77] chore: Adds a settings button to the notification settings from the notification popup (#6233) --- .../components/NotificationPanel.vue | 28 +++++++++++++++++-- .../settings/profile/NotificationSettings.vue | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue index ee31d94b2..7bed5ba88 100644 --- a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue @@ -25,6 +25,14 @@ > {{ $t('NOTIFICATIONS_PAGE.MARK_ALL_DONE') }} + { + const audioSettings = document.getElementById( + 'profile-settings-notifications' + ); + if (audioSettings) { + // TODO [ref](https://github.com/chatwoot/chatwoot/pull/6233#discussion_r1069636890) + audioSettings.scrollIntoView( + { behavior: 'smooth', block: 'start' }, + 150 + ); + } + }); + }, closeNotificationPanel() { this.$emit('close'); }, @@ -244,13 +268,13 @@ export default { .total-count { padding: var(--space-smaller) var(--space-small); background: var(--b-50); - border-radius: var(--border-radius-rounded); + border-radius: var(--border-radius-normal); font-size: var(--font-size-micro); font-weight: var(--font-weight-bold); + margin-left: var(--space-smaller); } .action-button { - padding: var(--space-micro) var(--space-small); margin-right: var(--space-small); } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue index f6b216b96..5e287a686 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue @@ -1,5 +1,5 @@