Merge pull request #316 from shariquerik/posthog-telemetry

feat: Init posthog telemetry to analyse crm usage
This commit is contained in:
Shariq Ansari 2024-08-16 20:36:46 +05:30 committed by GitHub
commit 73d9231bbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 209 additions and 28 deletions

View File

@ -2,6 +2,7 @@ from bs4 import BeautifulSoup
import frappe import frappe
from frappe.translate import get_all_translations from frappe.translate import get_all_translations
from frappe.utils import cstr from frappe.utils import cstr
from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@ -44,4 +45,13 @@ def get_user_signature():
content = "" content = ""
if (cstr(_signature) or signature): if (cstr(_signature) or signature):
content = f'<br><p class="signature">{signature}</p>' content = f'<br><p class="signature">{signature}</p>'
return content return content
@frappe.whitelist()
def get_posthog_settings():
return {
"posthog_project_id": frappe.conf.get(POSTHOG_PROJECT_FIELD),
"posthog_host": frappe.conf.get(POSTHOG_HOST_FIELD),
"enable_telemetry": frappe.get_system_settings("enable_telemetry"),
"telemetry_site_age": frappe.utils.telemetry.site_age(),
}

View File

@ -437,6 +437,7 @@ import { globalStore } from '@/stores/global'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts' import { contactsStore } from '@/stores/contacts'
import { whatsappEnabled } from '@/composables/settings' import { whatsappEnabled } from '@/composables/settings'
import { capture } from '@/telemetry'
import { Button, Tooltip, createResource } from 'frappe-ui' import { Button, Tooltip, createResource } from 'frappe-ui'
import { useElementVisibility } from '@vueuse/core' import { useElementVisibility } from '@vueuse/core'
import { import {
@ -552,6 +553,7 @@ onMounted(() => {
function sendTemplate(template) { function sendTemplate(template) {
showWhatsappTemplates.value = false showWhatsappTemplates.value = false
capture('send_whatsapp_template', { doctype: props.doctype })
createResource({ createResource({
url: 'crm.api.whatsapp.send_whatsapp_template', url: 'crm.api.whatsapp.send_whatsapp_template',
params: { params: {

View File

@ -170,9 +170,9 @@ import DoubleCheckIcon from '@/components/Icons/DoubleCheckIcon.vue'
import DocumentIcon from '@/components/Icons/DocumentIcon.vue' import DocumentIcon from '@/components/Icons/DocumentIcon.vue'
import ReactIcon from '@/components/Icons/ReactIcon.vue' import ReactIcon from '@/components/Icons/ReactIcon.vue'
import { dateFormat } from '@/utils' import { dateFormat } from '@/utils'
import { capture } from '@/telemetry'
import { Tooltip, Dropdown, createResource } from 'frappe-ui' import { Tooltip, Dropdown, createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
import FeatherIcon from 'frappe-ui/src/components/FeatherIcon.vue'
const props = defineProps({ const props = defineProps({
messages: Array, messages: Array,
@ -219,6 +219,7 @@ function reactOnMessage(name, emoji) {
}, },
auto: true, auto: true,
onSuccess() { onSuccess() {
capture('whatsapp_react_on_message')
list.value.reload() list.value.reload()
}, },
}) })

View File

@ -39,6 +39,7 @@
() => { () => {
content += emoji content += emoji
$refs.textarea.$el.focus() $refs.textarea.$el.focus()
capture('whatsapp_emoji_added')
} }
" "
> >
@ -65,8 +66,8 @@
<script setup> <script setup>
import IconPicker from '@/components/IconPicker.vue' import IconPicker from '@/components/IconPicker.vue'
import SmileIcon from '@/components/Icons/SmileIcon.vue' import SmileIcon from '@/components/Icons/SmileIcon.vue'
import { capture } from '@/telemetry'
import { createResource, Textarea, FileUploader, Dropdown } from 'frappe-ui' import { createResource, Textarea, FileUploader, Dropdown } from 'frappe-ui'
import FeatherIcon from 'frappe-ui/src/components/FeatherIcon.vue'
import { ref, nextTick, watch } from 'vue' import { ref, nextTick, watch } from 'vue'
const props = defineProps({ const props = defineProps({
@ -92,6 +93,7 @@ function uploadFile(file) {
whatsapp.value.attach = file.file_url whatsapp.value.attach = file.file_url
whatsapp.value.content_type = fileType.value whatsapp.value.content_type = fileType.value
sendWhatsAppMessage() sendWhatsAppMessage()
capture('whatsapp_upload_file')
} }
function sendTextMessage(event) { function sendTextMessage(event) {
@ -99,6 +101,7 @@ function sendTextMessage(event) {
sendWhatsAppMessage() sendWhatsAppMessage()
textarea.value.$el.blur() textarea.value.$el.blur()
content.value = '' content.value = ''
capture('whatsapp_send_message')
} }
async function sendWhatsAppMessage() { async function sendWhatsAppMessage() {

View File

@ -39,6 +39,8 @@
<script setup> <script setup>
import AppsIcon from '@/components/Icons/AppsIcon.vue' import AppsIcon from '@/components/Icons/AppsIcon.vue'
import { Popover, createResource } from 'frappe-ui' import { Popover, createResource } from 'frappe-ui'
import { onUnmounted } from 'vue';
import { stopRecording } from '@/telemetry';
const props = defineProps({ const props = defineProps({
active: Boolean, active: Boolean,
@ -70,4 +72,8 @@ const apps = createResource({
return _apps return _apps
}, },
}) })
onUnmounted(() => {
stopRecording()
})
</script> </script>

View File

@ -197,6 +197,7 @@ import { Device } from '@twilio/voice-sdk'
import { useDraggable, useWindowSize } from '@vueuse/core' import { useDraggable, useWindowSize } from '@vueuse/core'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { contactsStore } from '@/stores/contacts' import { contactsStore } from '@/stores/contacts'
import { capture } from '@/telemetry'
import { Avatar, call } from 'frappe-ui' import { Avatar, call } from 'frappe-ui'
import { onMounted, ref, watch } from 'vue' import { onMounted, ref, watch } from 'vue'
@ -403,6 +404,8 @@ async function makeOutgoingCall(number) {
showCallPopup.value = true showCallPopup.value = true
callStatus.value = 'initiating' callStatus.value = 'initiating'
capture('make_outgoing_call')
_call.on('messageReceived', (message) => { _call.on('messageReceived', (message) => {
let info = message.content let info = message.content
callStatus.value = info.CallStatus callStatus.value = info.CallStatus

View File

@ -92,6 +92,7 @@ import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
import AttachmentItem from '@/components/AttachmentItem.vue' import AttachmentItem from '@/components/AttachmentItem.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { TextEditorBubbleMenu, TextEditor, FileUploader } from 'frappe-ui' import { TextEditorBubbleMenu, TextEditor, FileUploader } from 'frappe-ui'
import { capture } from '@/telemetry'
import { EditorContent } from '@tiptap/vue-3' import { EditorContent } from '@tiptap/vue-3'
import { ref, computed, defineModel } from 'vue' import { ref, computed, defineModel } from 'vue'
@ -139,6 +140,7 @@ function appendEmoji() {
editor.value.commands.insertContent(emoji.value) editor.value.commands.insertContent(emoji.value)
editor.value.commands.focus() editor.value.commands.focus()
emoji.value = '' emoji.value = ''
capture('emoji_inserted_in_comment', { emoji: emoji.value })
} }
function removeAttachment(attachment) { function removeAttachment(attachment) {

View File

@ -88,6 +88,7 @@ import EmailEditor from '@/components/EmailEditor.vue'
import CommentBox from '@/components/CommentBox.vue' import CommentBox from '@/components/CommentBox.vue'
import CommentIcon from '@/components/Icons/CommentIcon.vue' import CommentIcon from '@/components/Icons/CommentIcon.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue' import Email2Icon from '@/components/Icons/Email2Icon.vue'
import { capture } from '@/telemetry'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { call, createResource } from 'frappe-ui' import { call, createResource } from 'frappe-ui'
@ -176,6 +177,10 @@ async function sendMail() {
let subject = newEmailEditor.value.subject let subject = newEmailEditor.value.subject
let cc = newEmailEditor.value.ccEmails || [] let cc = newEmailEditor.value.ccEmails || []
let bcc = newEmailEditor.value.bccEmails || [] let bcc = newEmailEditor.value.bccEmails || []
if (attachments.value.length) {
capture('email_attachments_added')
}
await call('frappe.core.doctype.communication.email.make', { await call('frappe.core.doctype.communication.email.make', {
recipients: recipients.join(', '), recipients: recipients.join(', '),
attachments: attachments.value.map((x) => x.name), attachments: attachments.value.map((x) => x.name),
@ -200,6 +205,7 @@ async function sendComment() {
comment_by: getUser()?.full_name || undefined, comment_by: getUser()?.full_name || undefined,
}) })
if (comment && attachments.value.length) { if (comment && attachments.value.length) {
capture('comment_attachments_added')
await call('crm.api.comment.add_attachments', { await call('crm.api.comment.add_attachments', {
name: comment.name, name: comment.name,
attachments: attachments.value.map((x) => x.name), attachments: attachments.value.map((x) => x.name),
@ -214,6 +220,7 @@ async function submitEmail() {
newEmail.value = '' newEmail.value = ''
reload.value = true reload.value = true
emit('scroll') emit('scroll')
capture('email_sent', { doctype: props.doctype })
} }
async function submitComment() { async function submitComment() {
@ -223,6 +230,7 @@ async function submitComment() {
newComment.value = '' newComment.value = ''
reload.value = true reload.value = true
emit('scroll') emit('scroll')
capture('comment_sent', { doctype: props.doctype })
} }
function toggleEmailBox() { function toggleEmailBox() {

View File

@ -175,6 +175,7 @@ import AttachmentItem from '@/components/AttachmentItem.vue'
import MultiselectInput from '@/components/Controls/MultiselectInput.vue' import MultiselectInput from '@/components/Controls/MultiselectInput.vue'
import EmailTemplateSelectorModal from '@/components/Modals/EmailTemplateSelectorModal.vue' import EmailTemplateSelectorModal from '@/components/Modals/EmailTemplateSelectorModal.vue'
import { TextEditorBubbleMenu, TextEditor, FileUploader, call } from 'frappe-ui' import { TextEditorBubbleMenu, TextEditor, FileUploader, call } from 'frappe-ui'
import { capture } from '@/telemetry'
import { validateEmail } from '@/utils' import { validateEmail } from '@/utils'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import { EditorContent } from '@tiptap/vue-3' import { EditorContent } from '@tiptap/vue-3'
@ -273,12 +274,14 @@ async function applyEmailTemplate(template) {
editor.value.commands.setContent(data.message) editor.value.commands.setContent(data.message)
} }
showEmailTemplateSelectorModal.value = false showEmailTemplateSelectorModal.value = false
capture('email_template_applied', { doctype: props.doctype })
} }
function appendEmoji() { function appendEmoji() {
editor.value.commands.insertContent(emoji.value) editor.value.commands.insertContent(emoji.value)
editor.value.commands.focus() editor.value.commands.focus()
emoji.value = '' emoji.value = ''
capture('emoji_inserted_in_email', { emoji: emoji.value })
} }
function toggleCC() { function toggleCC() {

View File

@ -21,6 +21,7 @@ import EditValueModal from '@/components/Modals/EditValueModal.vue'
import AssignmentModal from '@/components/Modals/AssignmentModal.vue' import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
import { setupListActions, createToast } from '@/utils' import { setupListActions, createToast } from '@/utils'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { capture } from '@/telemetry'
import { call } from 'frappe-ui' import { call } from 'frappe-ui'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -69,6 +70,7 @@ function convertToDeal(selections, unselectAll) {
label: __('Convert'), label: __('Convert'),
variant: 'solid', variant: 'solid',
onClick: (close) => { onClick: (close) => {
capture('bulk_convert_to_deal')
Array.from(selections).forEach((name) => { Array.from(selections).forEach((name) => {
call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', { call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
lead: name, lead: name,
@ -103,6 +105,7 @@ function deleteValues(selections, unselectAll) {
variant: 'solid', variant: 'solid',
theme: 'red', theme: 'red',
onClick: (close) => { onClick: (close) => {
capture('bulk_delete')
call('frappe.desk.reportview.delete_items', { call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)), items: JSON.stringify(Array.from(selections)),
doctype: props.doctype, doctype: props.doctype,
@ -145,6 +148,7 @@ function clearAssignemnts(selections, unselectAll) {
variant: 'solid', variant: 'solid',
theme: 'red', theme: 'red',
onClick: (close) => { onClick: (close) => {
capture('bulk_clear_assignment')
call('frappe.desk.form.assign_to.remove_multiple', { call('frappe.desk.form.assign_to.remove_multiple', {
doctype: props.doctype, doctype: props.doctype,
names: JSON.stringify(Array.from(selections)), names: JSON.stringify(Array.from(selections)),

View File

@ -80,6 +80,7 @@
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { capture } from '@/telemetry'
import { Tooltip, call } from 'frappe-ui' import { Tooltip, call } from 'frappe-ui'
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
@ -161,6 +162,7 @@ function updateAssignees() {
if (addedAssignees.length) { if (addedAssignees.length) {
if (props.docs.size) { if (props.docs.size) {
capture('bulk_assign_to', { doctype: props.doctype })
call('frappe.desk.form.assign_to.add_multiple', { call('frappe.desk.form.assign_to.add_multiple', {
doctype: props.doctype, doctype: props.doctype,
name: JSON.stringify(Array.from(props.docs)), name: JSON.stringify(Array.from(props.docs)),
@ -171,6 +173,7 @@ function updateAssignees() {
emit('reload') emit('reload')
}) })
} else { } else {
capture('assign_to', { doctype: props.doctype })
call('frappe.desk.form.assign_to.add', { call('frappe.desk.form.assign_to.add', {
doctype: props.doctype, doctype: props.doctype,
name: props.doc.name, name: props.doc.name,

View File

@ -92,6 +92,7 @@ import CertificateIcon from '@/components/Icons/CertificateIcon.vue'
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import Dropdown from '@/components/frappe-ui/Dropdown.vue' import Dropdown from '@/components/frappe-ui/Dropdown.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { capture } from '@/telemetry'
import { call, createResource } from 'frappe-ui' import { call, createResource } from 'frappe-ui'
import { ref, nextTick, watch, computed } from 'vue' import { ref, nextTick, watch, computed } from 'vue'
import { createToast } from '@/utils' import { createToast } from '@/utils'
@ -160,7 +161,10 @@ async function callInsertDoc() {
..._contact.value, ..._contact.value,
}, },
}) })
doc.name && handleContactUpdate(doc) if (doc.name) {
capture('contact_created')
handleContactUpdate(doc)
}
} }
function handleContactUpdate(doc) { function handleContactUpdate(doc) {

View File

@ -61,6 +61,7 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
import Fields from '@/components/Fields.vue' import Fields from '@/components/Fields.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { capture } from '@/telemetry'
import { Switch, createResource } from 'frappe-ui' import { Switch, createResource } from 'frappe-ui'
import { computed, ref, reactive, onMounted, nextTick } from 'vue' import { computed, ref, reactive, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -201,6 +202,7 @@ function createDeal() {
isDealCreating.value = true isDealCreating.value = true
}, },
onSuccess(name) { onSuccess(name) {
capture('deal_created')
isDealCreating.value = false isDealCreating.value = false
show.value = false show.value = false
router.push({ name: 'Deal', params: { dealId: name } }) router.push({ name: 'Deal', params: { dealId: name } })

View File

@ -36,6 +36,7 @@
<script setup> <script setup>
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue' import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import { capture } from '@/telemetry'
import { FormControl, call, createResource, TextEditor, DatePicker } from 'frappe-ui' import { FormControl, call, createResource, TextEditor, DatePicker } from 'frappe-ui'
import { ref, computed, onMounted, h } from 'vue' import { ref, computed, onMounted, h } from 'vue'
@ -115,6 +116,7 @@ function updateValues() {
newValue.value = '' newValue.value = ''
loading.value = false loading.value = false
show.value = false show.value = false
capture('bulk_update', { doctype: props.doctype })
emit('reload') emit('reload')
}) })
} }

View File

@ -96,6 +96,7 @@
</template> </template>
<script setup> <script setup>
import { capture } from '@/telemetry'
import { Checkbox, Select, TextEditor, call } from 'frappe-ui' import { Checkbox, Select, TextEditor, call } from 'frappe-ui'
import { ref, nextTick, watch } from 'vue' import { ref, nextTick, watch } from 'vue'
@ -171,7 +172,10 @@ async function callInsertDoc() {
..._emailTemplate.value, ..._emailTemplate.value,
}, },
}) })
doc.name && handleEmailTemplateUpdate(doc) if (doc.name) {
capture('email_template_created', { doctype: doc.reference_doctype })
handleEmailTemplateUpdate(doc)
}
} }
function handleEmailTemplateUpdate(doc) { function handleEmailTemplateUpdate(doc) {

View File

@ -46,6 +46,7 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
import Fields from '@/components/Fields.vue' import Fields from '@/components/Fields.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { capture } from '@/telemetry'
import { createResource } from 'frappe-ui' import { createResource } from 'frappe-ui'
import { computed, onMounted, ref, reactive, nextTick } from 'vue' import { computed, onMounted, ref, reactive, nextTick } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -153,6 +154,7 @@ function createNewLead() {
isLeadCreating.value = true isLeadCreating.value = true
}, },
onSuccess(data) { onSuccess(data) {
capture('lead_created')
isLeadCreating.value = false isLeadCreating.value = false
show.value = false show.value = false
router.push({ name: 'Lead', params: { leadId: data.name } }) router.push({ name: 'Lead', params: { leadId: data.name } })

View File

@ -66,6 +66,7 @@
<script setup> <script setup>
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue' import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
import { capture } from '@/telemetry'
import { TextEditor, call } from 'frappe-ui' import { TextEditor, call } from 'frappe-ui'
import { ref, nextTick, watch } from 'vue' import { ref, nextTick, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -124,6 +125,7 @@ async function updateNote() {
}, },
}) })
if (d.name) { if (d.name) {
capture('note_created')
notes.value?.reload() notes.value?.reload()
emit('after', d, true) emit('after', d, true)
} }

View File

@ -67,6 +67,7 @@ import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue' import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { formatNumberIntoCurrency } from '@/utils' import { formatNumberIntoCurrency } from '@/utils'
import { capture } from '@/telemetry'
import { call, FeatherIcon, createResource } from 'frappe-ui' import { call, FeatherIcon, createResource } from 'frappe-ui'
import { ref, nextTick, watch, computed, h } from 'vue' import { ref, nextTick, watch, computed, h } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -157,7 +158,10 @@ async function callInsertDoc() {
}, },
}) })
loading.value = false loading.value = false
doc.name && handleOrganizationUpdate(doc) if (doc.name) {
capture('organization_created')
handleOrganizationUpdate(doc)
}
} }
function handleOrganizationUpdate(doc, renamed = false) { function handleOrganizationUpdate(doc, renamed = false) {

View File

@ -118,6 +118,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { taskStatusOptions, taskPriorityOptions } from '@/utils' import { taskStatusOptions, taskPriorityOptions } from '@/utils'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { capture } from '@/telemetry'
import { TextEditor, Dropdown, Tooltip, call, DateTimePicker } from 'frappe-ui' import { TextEditor, Dropdown, Tooltip, call, DateTimePicker } from 'frappe-ui'
import { ref, watch, nextTick, onMounted } from 'vue' import { ref, watch, nextTick, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -199,6 +200,7 @@ async function updateTask() {
}, },
}) })
if (d.name) { if (d.name) {
capture('task_created')
tasks.value.reload() tasks.value.reload()
} }
} }

View File

@ -75,7 +75,6 @@ const templates = createListResource({
filters: { status: 'APPROVED', for_doctype: ['in', [props.doctype, '']] }, filters: { status: 'APPROVED', for_doctype: ['in', [props.doctype, '']] },
orderBy: 'modified desc', orderBy: 'modified desc',
pageLength: 99999, pageLength: 99999,
auto: true,
}) })
onMounted(() => { onMounted(() => {

View File

@ -18,10 +18,7 @@
<div class="flex gap-1"> <div class="flex gap-1">
<Tooltip :text="__('Mark all as read')"> <Tooltip :text="__('Mark all as read')">
<div> <div>
<Button <Button variant="ghost" @click="() => markAllAsRead()">
variant="ghost"
@click="() => notificationsStore().mark_as_read.reload()"
>
<template #icon> <template #icon>
<MarkAsDoneIcon class="h-4 w-4" /> <MarkAsDoneIcon class="h-4 w-4" />
</template> </template>
@ -48,7 +45,7 @@
:key="n.comment" :key="n.comment"
:to="getRoute(n)" :to="getRoute(n)"
class="flex cursor-pointer items-start gap-2.5 px-4 py-2.5 hover:bg-gray-100" class="flex cursor-pointer items-start gap-2.5 px-4 py-2.5 hover:bg-gray-100"
@click="mark_as_read(n.comment || n.notification_type_doc)" @click="markAsRead(n.comment || n.notification_type_doc)"
> >
<div class="mt-1 flex items-center gap-2.5"> <div class="mt-1 flex items-center gap-2.5">
<div <div
@ -98,6 +95,7 @@ import { notificationsStore } from '@/stores/notifications'
import { globalStore } from '@/stores/global' import { globalStore } from '@/stores/global'
import { timeAgo } from '@/utils' import { timeAgo } from '@/utils'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import { capture } from '@/telemetry'
import { Tooltip } from 'frappe-ui' import { Tooltip } from 'frappe-ui'
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
@ -113,17 +111,23 @@ onClickOutside(
}, },
{ {
ignore: ['#notifications-btn'], ignore: ['#notifications-btn'],
} },
) )
function toggleNotificationPanel() { function toggleNotificationPanel() {
notificationsStore().toggle() notificationsStore().toggle()
} }
function mark_as_read(doc) { function markAsRead(doc) {
capture('notification_mark_as_read')
notificationsStore().mark_doc_as_read(doc) notificationsStore().mark_doc_as_read(doc)
} }
function markAllAsRead() {
capture('notification_mark_all_as_read')
notificationsStore().mark_as_read.reload()
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
$socket.off('crm_notification') $socket.off('crm_notification')
}) })

View File

@ -5,7 +5,9 @@
:key="s.label" :key="s.label"
class="flex items-center gap-2 text-base leading-5" class="flex items-center gap-2 text-base leading-5"
> >
<div class="sm:w-[106px] w-36 text-sm text-gray-600">{{ __(s.label) }}</div> <div class="sm:w-[106px] w-36 text-sm text-gray-600">
{{ __(s.label) }}
</div>
<div class="grid min-h-[28px] items-center"> <div class="grid min-h-[28px] items-center">
<Tooltip v-if="s.tooltipText" :text="__(s.tooltipText)"> <Tooltip v-if="s.tooltipText" :text="__(s.tooltipText)">
<div class="ml-2 cursor-pointer"> <div class="ml-2 cursor-pointer">
@ -43,6 +45,7 @@
import { Dropdown, Tooltip } from 'frappe-ui' import { Dropdown, Tooltip } from 'frappe-ui'
import { timeAgo, dateFormat, formatTime, dateTooltipFormat } from '@/utils' import { timeAgo, dateFormat, formatTime, dateTooltipFormat } from '@/utils'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { capture } from '@/telemetry'
import { computed, defineModel } from 'vue' import { computed, defineModel } from 'vue'
const data = defineModel() const data = defineModel()
@ -58,8 +61,8 @@ let slaSection = computed(() => {
data.value.sla_status == 'Failed' data.value.sla_status == 'Failed'
? 'red' ? 'red'
: data.value.sla_status == 'Fulfilled' : data.value.sla_status == 'Fulfilled'
? 'green' ? 'green'
: 'orange' : 'orange'
if (status == 'First Response Due') { if (status == 'First Response Due') {
status = timeAgo(data.value.response_by) status = timeAgo(data.value.response_by)
@ -94,11 +97,13 @@ let slaSection = computed(() => {
options: communicationStatuses.data?.map((status) => ({ options: communicationStatuses.data?.map((status) => ({
label: status.name, label: status.name,
value: status.name, value: status.name,
onClick: () => onClick: () => {
emit('updateField', 'communication_status', status.name), capture('sla_status_change')
emit('updateField', 'communication_status', status.name)
},
})), })),
}, },
] ],
) )
return sections return sections
}) })

View File

@ -56,6 +56,7 @@
import Fields from '@/components/Fields.vue' import Fields from '@/components/Fields.vue'
import QuickEntryLayoutBuilder from '@/components/Settings/QuickEntryLayoutBuilder.vue' import QuickEntryLayoutBuilder from '@/components/Settings/QuickEntryLayoutBuilder.vue'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn } from '@vueuse/core'
import { capture } from '@/telemetry'
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui' import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
import { ref, watch, onMounted, nextTick } from 'vue' import { ref, watch, onMounted, nextTick } from 'vue'
@ -122,6 +123,7 @@ function saveChanges() {
).then(() => { ).then(() => {
loading.value = false loading.value = false
show.value = false show.value = false
capture('quick_entry_layout_builder', { doctype: _doctype.value })
}) })
} }
</script> </script>

View File

@ -74,6 +74,7 @@ import Section from '@/components/Section.vue'
import SectionFields from '@/components/SectionFields.vue' import SectionFields from '@/components/SectionFields.vue'
import SidePanelLayoutBuilder from '@/components/Settings/SidePanelLayoutBuilder.vue' import SidePanelLayoutBuilder from '@/components/Settings/SidePanelLayoutBuilder.vue'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn } from '@vueuse/core'
import { capture } from '@/telemetry'
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui' import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
import { ref, watch, onMounted, nextTick } from 'vue' import { ref, watch, onMounted, nextTick } from 'vue'
@ -143,6 +144,7 @@ function saveChanges() {
).then(() => { ).then(() => {
loading.value = false loading.value = false
show.value = false show.value = false
capture('side_panel_layout_builder', { doctype: _doctype.value })
emit('reload') emit('reload')
}) })
} }

View File

@ -1,10 +1,5 @@
import './index.css' import './index.css'
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
import { createPinia } from 'pinia'
import { import {
FrappeUI, FrappeUI,
Button, Button,
@ -19,9 +14,14 @@ import {
frappeRequest, frappeRequest,
FeatherIcon, FeatherIcon,
} from 'frappe-ui' } from 'frappe-ui'
import translationPlugin from './translation' import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createDialog } from './utils/dialogs' import { createDialog } from './utils/dialogs'
import { initSocket } from './socket' import { initSocket } from './socket'
import router from './router'
import translationPlugin from './translation'
import { posthogPlugin } from './telemetry'
import App from './App.vue'
let globalComponents = { let globalComponents = {
Button, Button,
@ -45,6 +45,7 @@ app.use(FrappeUI)
app.use(pinia) app.use(pinia)
app.use(router) app.use(router)
app.use(translationPlugin) app.use(translationPlugin)
app.use(posthogPlugin)
for (let key in globalComponents) { for (let key in globalComponents) {
app.component(key, globalComponents[key]) app.component(key, globalComponents[key])
} }
@ -61,7 +62,7 @@ if (import.meta.env.DEV) {
socket = initSocket() socket = initSocket()
app.config.globalProperties.$socket = socket app.config.globalProperties.$socket = socket
app.mount('#app') app.mount('#app')
} },
) )
} else { } else {
socket = initSocket() socket = initSocket()

View File

@ -312,6 +312,7 @@ import { organizationsStore } from '@/stores/organizations'
import { statusesStore } from '@/stores/statuses' import { statusesStore } from '@/stores/statuses'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { whatsappEnabled, callEnabled } from '@/composables/settings' import { whatsappEnabled, callEnabled } from '@/composables/settings'
import { capture } from '@/telemetry'
import { import {
createResource, createResource,
FileUploader, FileUploader,
@ -587,6 +588,7 @@ async function convertToDeal(updated) {
}, },
) )
if (deal) { if (deal) {
capture('convert_lead_to_deal')
if (updated) { if (updated) {
await organizations.reload() await organizations.reload()
await contacts.reload() await contacts.reload()

View File

@ -1,7 +1,8 @@
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import { capture } from '@/telemetry'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { createListResource } from 'frappe-ui' import { createListResource } from 'frappe-ui'
import { reactive, h } from 'vue' import { reactive, h } from 'vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
export const statusesStore = defineStore('crm-statuses', () => { export const statusesStore = defineStore('crm-statuses', () => {
let leadStatusesByName = reactive({}) let leadStatusesByName = reactive({})
@ -103,6 +104,7 @@ export const statusesStore = defineStore('crm-statuses', () => {
class: statusesByName[status].iconColorClass, class: statusesByName[status].iconColorClass,
}), }),
onClick: () => { onClick: () => {
capture('status_changed', { doctype, status })
action && action('status', statusesByName[status].name) action && action('status', statusesByName[status].name)
}, },
}) })

97
frontend/src/telemetry.ts Normal file
View File

@ -0,0 +1,97 @@
import '../../../frappe/frappe/public/js/lib/posthog.js'
import { createResource } from 'frappe-ui'
import { computed } from 'vue'
declare global {
interface Window {
posthog: any
}
}
type PosthogSettings = {
posthog_project_id: string
posthog_host: string
enable_telemetry: boolean
telemetry_site_age: number
}
interface CaptureOptions {
data: {
user: string
[key: string]: string | number | boolean | object
}
}
let posthog: typeof window.posthog = window.posthog
// Posthog Settings
let posthogSettings = createResource({
url: 'crm.api.get_posthog_settings',
cache: 'posthog_settings',
onSuccess: (ps: PosthogSettings) => initPosthog(ps),
})
let isTelemetryEnabled = () => {
if (!posthogSettings.data) return false
return (
posthogSettings.data.enable_telemetry &&
posthogSettings.data.posthog_project_id &&
posthogSettings.data.posthog_host
)
}
// Posthog Initialization
function initPosthog(ps: PosthogSettings) {
if (!isTelemetryEnabled()) return
posthog.init(ps.posthog_project_id, {
api_host: ps.posthog_host,
person_profiles: 'identified_only',
autocapture: false,
capture_pageview: true,
capture_pageleave: true,
enable_heatmaps: false,
disable_session_recording: false,
loaded: (ph: typeof posthog) => {
window.posthog = ph
ph.identify(window.location.hostname)
},
})
}
// Posthog Functions
function capture(
event: string,
options: CaptureOptions = { data: { user: '' } },
) {
if (!isTelemetryEnabled()) return
window.posthog.capture(`crm_${event}`, options)
}
function startRecording() {
if (!isTelemetryEnabled()) return
if (window.posthog?.__loaded) {
window.posthog.startSessionRecording()
}
}
function stopRecording() {
if (!isTelemetryEnabled()) return
if (window.posthog?.__loaded && window.posthog.sessionRecordingStarted()) {
window.posthog.stopSessionRecording()
}
}
// Posthog Plugin
function posthogPlugin(app: any) {
app.config.globalProperties.posthog = posthog
if (!window.posthog?.length) posthogSettings.fetch()
}
export {
posthog,
posthogSettings,
posthogPlugin,
capture,
startRecording,
stopRecording,
}