feat: more events tracking for SaaS (#6234)

This commit is contained in:
Shivam Mishra
2023-01-18 11:23:40 +05:30
committed by GitHub
parent 1df1b1f8e4
commit 37b9816827
40 changed files with 539 additions and 50 deletions

View File

@@ -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;
},

View File

@@ -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);
},

View File

@@ -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',
});

View File

@@ -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);

View File

@@ -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);
},
};

View File

@@ -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();
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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');
});
});

View File

@@ -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,

View File

@@ -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;
},
},

View File

@@ -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(

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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'));

View File

@@ -31,6 +31,7 @@
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default {
mixins: [alertMixin],
@@ -89,6 +90,10 @@ export default {
? this.$t('FILTER.CUSTOM_VIEWS.ADD.API_FOLDERS.SUCCESS_MESSAGE')
: this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.SUCCESS_MESSAGE');
this.onClose();
this.$track(CONTACTS_EVENTS.SAVE_FILTER, {
type: this.filterType === 0 ? 'folder' : 'segment',
});
} catch (error) {
const errorMessage = error?.message;
this.alertMessage =

View File

@@ -16,6 +16,7 @@
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default {
mixins: [alertMixin],
props: {
@@ -82,6 +83,9 @@ export default {
? this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_FOLDERS.SUCCESS_MESSAGE')
: this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE')
);
this.$track(CONTACTS_EVENTS.DELETE_FILTER, {
type: this.filterType === 0 ? 'folder' : 'segment',
});
} catch (error) {
const errorMessage =
error?.response?.message || this.activeFilterType === 0

View File

@@ -42,6 +42,7 @@ import Modal from 'dashboard/components/Modal';
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators';
import allLocales from 'shared/constants/locales.js';
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
export default {
components: {
Modal,
@@ -108,6 +109,11 @@ export default {
'HELP_CENTER.PORTAL.ADD_LOCALE.API.SUCCESS_MESSAGE'
);
this.onClose();
this.$track(PORTALS_EVENTS.CREATE_LOCALE, {
localeAdded: this.selectedLocale,
totalLocales: updatedLocales.length,
from: this.$route.name,
});
} catch (error) {
this.alertMessage =
error?.message ||

View File

@@ -107,6 +107,7 @@
import alertMixin from 'shared/mixins/alertMixin';
import { mixin as clickaway } from 'vue-clickaway';
import wootConstants from 'dashboard/constants.js';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
const { ARTICLE_STATUS_TYPES } = wootConstants;
@@ -186,6 +187,11 @@ export default {
});
this.statusUpdateSuccessMessage(status);
this.closeActionsDropdown();
if (status === this.ARTICLE_STATUS_TYPES.ARCHIVE) {
this.$track(PORTALS_EVENTS.ARCHIVE_ARTICLE, { uiFrom: 'header' });
} else if (status === this.ARTICLE_STATUS_TYPES.PUBLISH) {
this.$track(PORTALS_EVENTS.PUBLISH_ARTICLE);
}
} catch (error) {
this.alertMessage =
error?.message || this.statusUpdateErrorMessage(status);

View File

@@ -190,6 +190,7 @@ import thumbnail from 'dashboard/components/widgets/Thumbnail';
import LocaleItemTable from './PortalListItemTable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import alertMixin from 'shared/mixins/alertMixin';
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
export default {
components: {
@@ -309,6 +310,10 @@ export default {
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.ERROR_MESSAGE'
),
});
this.$track(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
newLocale: localeCode,
from: this.$route.name,
});
},
deletePortalLocale({ localeCode }) {
const updatedLocales = this.allowedLocales.filter(
@@ -325,6 +330,10 @@ export default {
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.ERROR_MESSAGE'
),
});
this.$track(PORTALS_EVENTS.DELETE_LOCALE, {
deletedLocale: localeCode,
from: this.$route.name,
});
},
async updatePortalLocales({
allowedLocales,

View File

@@ -53,6 +53,7 @@ import portalMixin from '../../mixins/portalMixin';
import alertMixin from 'shared/mixins/alertMixin';
import wootConstants from 'dashboard/constants';
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
const { ARTICLE_STATUS_TYPES } = wootConstants;
export default {
@@ -118,6 +119,9 @@ export default {
confirmDeletion() {
this.closeDeletePopup();
this.deleteArticle();
this.$track(PORTALS_EVENTS.DELETE_ARTICLE, {
status: this.article?.status,
});
},
async saveArticle({ ...values }) {
this.isUpdating = true;
@@ -171,6 +175,7 @@ export default {
status: ARTICLE_STATUS_TYPES.ARCHIVE,
});
this.alertMessage = this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.SUCCESS');
this.$track(PORTALS_EVENTS.ARCHIVE_ARTICLE, { uiFrom: 'sidebar' });
} catch (error) {
this.alertMessage =
error?.message || this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.ERROR');
@@ -186,6 +191,9 @@ export default {
},
showArticleInPortal() {
window.open(this.portalLink, '_blank');
this.$track(PORTALS_EVENTS.PREVIEW_ARTICLE, {
status: this.article?.status,
});
},
},
};

View File

@@ -29,6 +29,7 @@ import ArticleEditor from '../../components/ArticleEditor.vue';
import portalMixin from '../../mixins/portalMixin';
import alertMixin from 'shared/mixins/alertMixin.js';
import ArticleSettings from './ArticleSettings.vue';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
EditArticleHeader,
@@ -91,6 +92,9 @@ export default {
recentlyCreated: true,
},
});
this.$track(PORTALS_EVENTS.CREATE_ARTICLE, {
locale: this.locale,
});
} catch (error) {
this.alertMessage =
error?.message ||

View File

@@ -70,6 +70,7 @@
import alertMixin from 'shared/mixins/alertMixin';
import { required, minLength } from 'vuelidate/lib/validators';
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
mixins: [alertMixin],
@@ -158,6 +159,9 @@ export default {
'HELP_CENTER.CATEGORY.ADD.API.SUCCESS_MESSAGE'
);
this.onClose();
this.$track(PORTALS_EVENTS.CREATE_CATEGORY, {
hasDescription: Boolean(description),
});
} catch (error) {
const errorMessage = error?.message;
this.alertMessage =

View File

@@ -58,7 +58,7 @@
variant="smooth"
icon="delete"
color-scheme="alert"
@click="deleteCategory(category.id)"
@click="deleteCategory(category)"
/>
</td>
</tr>
@@ -83,8 +83,8 @@ export default {
editCategory(category) {
this.$emit('edit', category);
},
deleteCategory(categoryId) {
this.$emit('delete', categoryId);
deleteCategory(category) {
this.$emit('delete', category);
},
},
};

View File

@@ -70,6 +70,7 @@
import alertMixin from 'shared/mixins/alertMixin';
import { required, minLength } from 'vuelidate/lib/validators';
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
mixins: [alertMixin],
@@ -166,6 +167,7 @@ export default {
this.alertMessage = this.$t(
'HELP_CENTER.CATEGORY.EDIT.API.SUCCESS_MESSAGE'
);
this.$track(PORTALS_EVENTS.EDIT_CATEGORY);
this.onClose();
} catch (error) {
const errorMessage = error?.message;

View File

@@ -64,6 +64,7 @@ import portalMixin from '../../mixins/portalMixin';
import CategoryListItem from './CategoryListItem';
import AddCategory from './AddCategory';
import EditCategory from './EditCategory';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
@@ -137,15 +138,18 @@ export default {
locale: localeCode,
});
},
async deleteCategory(categoryId) {
async deleteCategory(category) {
try {
await this.$store.dispatch('categories/delete', {
portalSlug: this.currentPortalSlug,
categoryId: categoryId,
categoryId: category.id,
});
this.alertMessage = this.$t(
'HELP_CENTER.CATEGORY.DELETE.API.SUCCESS_MESSAGE'
);
this.$track(PORTALS_EVENTS.DELETE_CATEGORY, {
hasArticles: category?.meta?.articles_count !== 0,
});
} catch (error) {
const errorMessage = error?.message;
this.alertMessage =

View File

@@ -36,6 +36,7 @@ import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import LocaleItemTable from 'dashboard/routes/dashboard/helpcenter/components/PortalListItemTable.vue';
import AddLocale from 'dashboard/routes/dashboard/helpcenter/components/AddLocale';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
LocaleItemTable,
@@ -85,6 +86,10 @@ export default {
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.ERROR_MESSAGE'
),
});
this.$track(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
newLocale: localeCode,
from: this.$route.name,
});
},
deletePortalLocale({ localeCode }) {
const updatedLocales = this.allowedLocales.filter(
@@ -101,6 +106,10 @@ export default {
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.ERROR_MESSAGE'
),
});
this.$track(PORTALS_EVENTS.DELETE_LOCALE, {
deletedLocale: localeCode,
from: this.$route.name,
});
},
async updatePortalLocales({
allowedLocales,

View File

@@ -17,6 +17,7 @@ import PortalSettingsCustomizationForm from 'dashboard/routes/dashboard/helpcent
import { mapGetters } from 'vuex';
import { getRandomColor } from 'dashboard/helper/labelColor';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
@@ -52,7 +53,6 @@ export default {
},
async updatePortalSettings(portalObj) {
const portalSlug = this.$route.params.portalSlug;
try {
await this.$store.dispatch('portals/update', {
portalSlug,
@@ -61,6 +61,12 @@ export default {
this.alertMessage = this.$t(
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE'
);
this.$track(PORTALS_EVENTS.ONBOARD_CUSTOMIZATION, {
hasHomePageLink: Boolean(portalObj.homepage_link),
hasPageTitle: Boolean(portalObj.page_title),
hasHeaderText: Boolean(portalObj.headerText),
});
} catch (error) {
this.alertMessage =
error?.message ||

View File

@@ -15,6 +15,7 @@ import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import PortalSettingsBasicForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
@@ -49,6 +50,11 @@ export default {
name: 'portal_customization',
params: { portalSlug: portal.slug },
});
const analyticsPayload = {
has_custom_domain: portal.domain !== '',
};
this.$track(PORTALS_EVENTS.ONBOARD_BASIC_INFORMATION, analyticsPayload);
this.$track(PORTALS_EVENTS.CREATE_PORTAL, analyticsPayload);
} catch (error) {
this.alertMessage =
error?.message ||

View File

@@ -177,6 +177,7 @@ import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import campaignMixin from 'shared/mixins/campaignMixin';
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
import { URLPattern } from 'urlpattern-polyfill';
import { CAMPAIGNS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
export default {
components: {
@@ -275,6 +276,11 @@ export default {
];
},
},
mounted() {
this.$track(CAMPAIGNS_EVENTS.OPEN_NEW_CAMPAIGN_MODAL, {
type: this.campaignType,
});
},
methods: {
onClose() {
this.$emit('on-close');
@@ -339,6 +345,12 @@ export default {
try {
const campaignDetails = this.getCampaignDetails();
await this.$store.dispatch('campaigns/create', campaignDetails);
// tracking this here instead of the store to track the type of campaign
this.$track(CAMPAIGNS_EVENTS.CREATE_CAMPAIGN, {
type: this.campaignType,
});
this.showAlert(this.$t('CAMPAIGN.ADD.API.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {

View File

@@ -25,6 +25,7 @@ import CsatTable from './components/CsatTable';
import ReportFilterSelector from './components/FilterSelector';
import { mapGetters } from 'vuex';
import { generateFileName } from '../../../../helper/downloadHelper';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
export default {
name: 'CsatResponses',
@@ -66,6 +67,13 @@ export default {
this.getResponses();
},
onDateRangeChange({ from, to }) {
// do not track filter change on inital load
if (this.from !== 0 && this.to !== 0) {
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'date',
reportType: 'csat',
});
}
this.from = from;
this.to = to;
this.getAllData();
@@ -73,6 +81,10 @@ export default {
onAgentsFilterChange(agents) {
this.userIds = agents.map(el => el.id);
this.getAllData();
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'agent',
reportType: 'csat',
});
},
downloadReports() {
const type = 'csat';

View File

@@ -58,6 +58,7 @@ import ReportFilterSelector from './components/FilterSelector';
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
import reportMixin from '../../../../mixins/reportMixin';
import { formatTime } from '@chatwoot/utils';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
@@ -222,6 +223,13 @@ export default {
this.fetchChartData();
},
onDateRangeChange({ from, to, groupBy }) {
// do not track filter change on inital load
if (this.from !== 0 && this.to !== 0) {
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'date',
reportType: 'conversations',
});
}
this.from = from;
this.to = to;
this.filterItemsList = this.fetchFilterItems(groupBy);
@@ -239,6 +247,12 @@ export default {
onFilterChange(payload) {
this.groupBy = GROUP_BY_FILTER[payload.id];
this.fetchAllData();
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'groupBy',
filterValue: this.groupBy?.period,
reportType: 'conversations',
});
},
fetchFilterItems(group_by) {
switch (group_by) {
@@ -255,6 +269,12 @@ export default {
onBusinessHoursToggle(value) {
this.businessHours = value;
this.fetchAllData();
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'businessHours',
filterValue: value,
reportType: 'conversations',
});
},
},
};

View File

@@ -62,6 +62,7 @@ import { GROUP_BY_FILTER, METRIC_CHART } from '../constants';
import reportMixin from '../../../../../mixins/reportMixin';
import { formatTime } from '@chatwoot/utils';
import { generateFileName } from '../../../../../helper/downloadHelper';
import { REPORTS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
@@ -269,6 +270,14 @@ export default {
this.fetchChartData();
},
onDateRangeChange({ from, to, groupBy }) {
// do not track filter change on inital load
if (this.from !== 0 && this.to !== 0) {
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'date',
reportType: this.type,
});
}
this.from = from;
this.to = to;
this.groupByfilterItemsList = this.fetchFilterItems(groupBy);
@@ -292,6 +301,12 @@ export default {
onGroupByFilterChange(payload) {
this.groupBy = GROUP_BY_FILTER[payload.id];
this.fetchAllData();
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'groupBy',
filterValue: this.groupBy?.period,
reportType: this.type,
});
},
fetchFilterItems(group_by) {
switch (group_by) {
@@ -308,6 +323,12 @@ export default {
onBusinessHoursToggle(value) {
this.businessHours = value;
this.fetchAllData();
this.$track(REPORTS_EVENTS.FILTER_REPORT, {
filterType: 'businessHours',
filterValue: value,
reportType: this.type,
});
},
},
};

View File

@@ -1,6 +1,8 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types';
import CampaignsAPI from '../../api/campaigns';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { CAMPAIGNS_EVENTS } from '../../helper/AnalyticsHelper/events';
export const state = {
records: [],
@@ -51,6 +53,7 @@ export const actions = {
commit(types.SET_CAMPAIGN_UI_FLAG, { isUpdating: true });
try {
const response = await CampaignsAPI.update(id, updateObj);
AnalyticsHelper.track(CAMPAIGNS_EVENTS.UPDATE_CAMPAIGN);
commit(types.EDIT_CAMPAIGN, response.data);
} catch (error) {
throw new Error(error);
@@ -62,6 +65,7 @@ export const actions = {
commit(types.SET_CAMPAIGN_UI_FLAG, { isDeleting: true });
try {
await CampaignsAPI.delete(id);
AnalyticsHelper.track(CAMPAIGNS_EVENTS.DELETE_CAMPAIGN);
commit(types.DELETE_CAMPAIGN, id);
} catch (error) {
throw new Error(error);

View File

@@ -5,6 +5,8 @@ import {
import types from '../../mutation-types';
import ContactAPI from '../../../api/contacts';
import AccountActionsAPI from '../../../api/accountActions';
import AnalyticsHelper from '../../../helper/AnalyticsHelper';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
const buildContactFormData = contactParams => {
const formData = new FormData();
@@ -104,6 +106,8 @@ export const actions = {
const response = await ContactAPI.create(
isFormData ? buildContactFormData(contactParams) : contactParams
);
AnalyticsHelper.track(CONTACTS_EVENTS.CREATE_CONTACT);
commit(types.SET_CONTACT_ITEM, response.data.payload.contact);
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
} catch (error) {

View File

@@ -10,9 +10,8 @@ import {
isOnUnattendedView,
} from './helpers/actionHelpers';
import messageReadActions from './actions/messageReadActions';
import AnalyticsHelper, {
ANALYTICS_EVENTS,
} from '../../../helper/AnalyticsHelper';
import AnalyticsHelper from '../../../helper/AnalyticsHelper';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
// actions
const actions = {
getConversation: async ({ commit }, conversationId) => {
@@ -176,8 +175,8 @@ const actions = {
const response = await MessageApi.create(pendingMessage);
AnalyticsHelper.track(
pendingMessage.private
? ANALYTICS_EVENTS.SENT_PRIVATE_NOTE
: ANALYTICS_EVENTS.SENT_MESSAGE
? CONVERSATION_EVENTS.CONVERSATION.SENT_PRIVATE_NOTE
: CONVERSATION_EVENTS.CONVERSATION.SENT_MESSAGE
);
commit(types.ADD_MESSAGE, {
...response.data,

View File

@@ -2,6 +2,8 @@ import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types';
import CSATReports from '../../api/csatReports';
import { downloadCsvFile } from '../../helper/downloadHelper';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
const computeDistribution = (value, total) =>
((value * 100) / total).toFixed(2);
@@ -111,6 +113,9 @@ export const actions = {
downloadCSATReports(_, params) {
return CSATReports.download(params).then(response => {
downloadCsvFile(params.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'csat',
});
});
},
};

View File

@@ -6,9 +6,8 @@ import WebChannel from '../../api/channel/webChannel';
import FBChannel from '../../api/channel/fbChannel';
import TwilioChannel from '../../api/channel/twilioChannel';
import { throwErrorMessage } from '../utils/api';
import AnalyticsHelper, {
ANALYTICS_EVENTS,
} from '../../helper/AnalyticsHelper';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { ACCOUNT_EVENTS } from '../../helper/AnalyticsHelper/events';
const buildInboxData = inboxParams => {
const formData = new FormData();
@@ -121,7 +120,7 @@ export const getters = {
};
const sendAnalyticsEvent = channelType => {
AnalyticsHelper.track(ANALYTICS_EVENTS.ADDED_AN_INBOX, {
AnalyticsHelper.track(ACCOUNT_EVENTS.ADDED_AN_INBOX, {
channelType,
});
};

View File

@@ -1,6 +1,8 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types';
import LabelsAPI from '../../api/labels';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { LABEL_EVENTS } from '../../helper/AnalyticsHelper/events';
export const state = {
records: [],
@@ -43,6 +45,7 @@ export const actions = {
commit(types.SET_LABEL_UI_FLAG, { isCreating: true });
try {
const response = await LabelsAPI.create(cannedObj);
AnalyticsHelper.track(LABEL_EVENTS.CREATE);
commit(types.ADD_LABEL, response.data);
} catch (error) {
const errorMessage = error?.response?.data?.message;
@@ -56,6 +59,7 @@ export const actions = {
commit(types.SET_LABEL_UI_FLAG, { isUpdating: true });
try {
const response = await LabelsAPI.update(id, updateObj);
AnalyticsHelper.track(LABEL_EVENTS.UPDATE);
commit(types.EDIT_LABEL, response.data);
} catch (error) {
throw new Error(error);
@@ -68,6 +72,7 @@ export const actions = {
commit(types.SET_LABEL_UI_FLAG, { isDeleting: true });
try {
await LabelsAPI.delete(id);
AnalyticsHelper.track(LABEL_EVENTS.DELETED);
commit(types.DELETE_LABEL, id);
} catch (error) {
throw new Error(error);

View File

@@ -2,6 +2,8 @@
import * as types from '../mutation-types';
import Report from '../../api/reports';
import { downloadCsvFile } from '../../helper/downloadHelper';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
const state = {
fetchingStatus: false,
@@ -116,6 +118,10 @@ export const actions = {
return Report.getAgentReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'agent',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);
@@ -125,6 +131,10 @@ export const actions = {
return Report.getLabelReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'label',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);
@@ -134,6 +144,10 @@ export const actions = {
return Report.getInboxReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'inbox',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);
@@ -143,6 +157,10 @@ export const actions = {
return Report.getTeamReports(reportObj)
.then(response => {
downloadCsvFile(reportObj.fileName, response.data);
AnalyticsHelper.track(REPORTS_EVENTS.DOWNLOAD_REPORT, {
reportType: 'team',
businessHours: reportObj?.businessHours,
});
})
.catch(error => {
console.error(error);

View File

@@ -29,7 +29,7 @@ import {
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
import AnalyticsHelper from '../dashboard/helper/AnalyticsHelper';
import AnalyticsPlugin from '../dashboard/helper/AnalyticsHelper/plugin';
Vue.config.env = process.env;
@@ -55,6 +55,7 @@ Vue.use(VTooltip, {
defaultHtml: false,
});
Vue.use(hljs.vuePlugin);
Vue.use(AnalyticsPlugin);
Vue.component('multiselect', Multiselect);
Vue.component('woot-switch', WootSwitch);
@@ -74,7 +75,6 @@ window.WootConstants = constants;
window.axios = createAxios(axios);
window.bus = new Vue();
initializeChatwootEvents();
AnalyticsHelper.init();
initializeAnalyticsEvents();
initalizeRouter();