Merge pull request #316 from shariquerik/posthog-telemetry
feat: Init posthog telemetry to analyse crm usage
This commit is contained in:
commit
73d9231bbe
@ -2,6 +2,7 @@ from bs4 import BeautifulSoup
|
||||
import frappe
|
||||
from frappe.translate import get_all_translations
|
||||
from frappe.utils import cstr
|
||||
from frappe.utils.telemetry import POSTHOG_HOST_FIELD, POSTHOG_PROJECT_FIELD
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@ -44,4 +45,13 @@ def get_user_signature():
|
||||
content = ""
|
||||
if (cstr(_signature) or signature):
|
||||
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(),
|
||||
}
|
||||
@ -437,6 +437,7 @@ import { globalStore } from '@/stores/global'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { whatsappEnabled } from '@/composables/settings'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Button, Tooltip, createResource } from 'frappe-ui'
|
||||
import { useElementVisibility } from '@vueuse/core'
|
||||
import {
|
||||
@ -552,6 +553,7 @@ onMounted(() => {
|
||||
|
||||
function sendTemplate(template) {
|
||||
showWhatsappTemplates.value = false
|
||||
capture('send_whatsapp_template', { doctype: props.doctype })
|
||||
createResource({
|
||||
url: 'crm.api.whatsapp.send_whatsapp_template',
|
||||
params: {
|
||||
|
||||
@ -170,9 +170,9 @@ import DoubleCheckIcon from '@/components/Icons/DoubleCheckIcon.vue'
|
||||
import DocumentIcon from '@/components/Icons/DocumentIcon.vue'
|
||||
import ReactIcon from '@/components/Icons/ReactIcon.vue'
|
||||
import { dateFormat } from '@/utils'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Tooltip, Dropdown, createResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import FeatherIcon from 'frappe-ui/src/components/FeatherIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
messages: Array,
|
||||
@ -219,6 +219,7 @@ function reactOnMessage(name, emoji) {
|
||||
},
|
||||
auto: true,
|
||||
onSuccess() {
|
||||
capture('whatsapp_react_on_message')
|
||||
list.value.reload()
|
||||
},
|
||||
})
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
() => {
|
||||
content += emoji
|
||||
$refs.textarea.$el.focus()
|
||||
capture('whatsapp_emoji_added')
|
||||
}
|
||||
"
|
||||
>
|
||||
@ -65,8 +66,8 @@
|
||||
<script setup>
|
||||
import IconPicker from '@/components/IconPicker.vue'
|
||||
import SmileIcon from '@/components/Icons/SmileIcon.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import { createResource, Textarea, FileUploader, Dropdown } from 'frappe-ui'
|
||||
import FeatherIcon from 'frappe-ui/src/components/FeatherIcon.vue'
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
@ -92,6 +93,7 @@ function uploadFile(file) {
|
||||
whatsapp.value.attach = file.file_url
|
||||
whatsapp.value.content_type = fileType.value
|
||||
sendWhatsAppMessage()
|
||||
capture('whatsapp_upload_file')
|
||||
}
|
||||
|
||||
function sendTextMessage(event) {
|
||||
@ -99,6 +101,7 @@ function sendTextMessage(event) {
|
||||
sendWhatsAppMessage()
|
||||
textarea.value.$el.blur()
|
||||
content.value = ''
|
||||
capture('whatsapp_send_message')
|
||||
}
|
||||
|
||||
async function sendWhatsAppMessage() {
|
||||
|
||||
@ -39,6 +39,8 @@
|
||||
<script setup>
|
||||
import AppsIcon from '@/components/Icons/AppsIcon.vue'
|
||||
import { Popover, createResource } from 'frappe-ui'
|
||||
import { onUnmounted } from 'vue';
|
||||
import { stopRecording } from '@/telemetry';
|
||||
|
||||
const props = defineProps({
|
||||
active: Boolean,
|
||||
@ -70,4 +72,8 @@ const apps = createResource({
|
||||
return _apps
|
||||
},
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopRecording()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -197,6 +197,7 @@ import { Device } from '@twilio/voice-sdk'
|
||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Avatar, call } from 'frappe-ui'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
@ -403,6 +404,8 @@ async function makeOutgoingCall(number) {
|
||||
showCallPopup.value = true
|
||||
callStatus.value = 'initiating'
|
||||
|
||||
capture('make_outgoing_call')
|
||||
|
||||
_call.on('messageReceived', (message) => {
|
||||
let info = message.content
|
||||
callStatus.value = info.CallStatus
|
||||
|
||||
@ -92,6 +92,7 @@ import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||
import AttachmentItem from '@/components/AttachmentItem.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { TextEditorBubbleMenu, TextEditor, FileUploader } from 'frappe-ui'
|
||||
import { capture } from '@/telemetry'
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import { ref, computed, defineModel } from 'vue'
|
||||
|
||||
@ -139,6 +140,7 @@ function appendEmoji() {
|
||||
editor.value.commands.insertContent(emoji.value)
|
||||
editor.value.commands.focus()
|
||||
emoji.value = ''
|
||||
capture('emoji_inserted_in_comment', { emoji: emoji.value })
|
||||
}
|
||||
|
||||
function removeAttachment(attachment) {
|
||||
|
||||
@ -88,6 +88,7 @@ import EmailEditor from '@/components/EmailEditor.vue'
|
||||
import CommentBox from '@/components/CommentBox.vue'
|
||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { call, createResource } from 'frappe-ui'
|
||||
@ -176,6 +177,10 @@ async function sendMail() {
|
||||
let subject = newEmailEditor.value.subject
|
||||
let cc = newEmailEditor.value.ccEmails || []
|
||||
let bcc = newEmailEditor.value.bccEmails || []
|
||||
|
||||
if (attachments.value.length) {
|
||||
capture('email_attachments_added')
|
||||
}
|
||||
await call('frappe.core.doctype.communication.email.make', {
|
||||
recipients: recipients.join(', '),
|
||||
attachments: attachments.value.map((x) => x.name),
|
||||
@ -200,6 +205,7 @@ async function sendComment() {
|
||||
comment_by: getUser()?.full_name || undefined,
|
||||
})
|
||||
if (comment && attachments.value.length) {
|
||||
capture('comment_attachments_added')
|
||||
await call('crm.api.comment.add_attachments', {
|
||||
name: comment.name,
|
||||
attachments: attachments.value.map((x) => x.name),
|
||||
@ -214,6 +220,7 @@ async function submitEmail() {
|
||||
newEmail.value = ''
|
||||
reload.value = true
|
||||
emit('scroll')
|
||||
capture('email_sent', { doctype: props.doctype })
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
@ -223,6 +230,7 @@ async function submitComment() {
|
||||
newComment.value = ''
|
||||
reload.value = true
|
||||
emit('scroll')
|
||||
capture('comment_sent', { doctype: props.doctype })
|
||||
}
|
||||
|
||||
function toggleEmailBox() {
|
||||
|
||||
@ -175,6 +175,7 @@ import AttachmentItem from '@/components/AttachmentItem.vue'
|
||||
import MultiselectInput from '@/components/Controls/MultiselectInput.vue'
|
||||
import EmailTemplateSelectorModal from '@/components/Modals/EmailTemplateSelectorModal.vue'
|
||||
import { TextEditorBubbleMenu, TextEditor, FileUploader, call } from 'frappe-ui'
|
||||
import { capture } from '@/telemetry'
|
||||
import { validateEmail } from '@/utils'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
@ -273,12 +274,14 @@ async function applyEmailTemplate(template) {
|
||||
editor.value.commands.setContent(data.message)
|
||||
}
|
||||
showEmailTemplateSelectorModal.value = false
|
||||
capture('email_template_applied', { doctype: props.doctype })
|
||||
}
|
||||
|
||||
function appendEmoji() {
|
||||
editor.value.commands.insertContent(emoji.value)
|
||||
editor.value.commands.focus()
|
||||
emoji.value = ''
|
||||
capture('emoji_inserted_in_email', { emoji: emoji.value })
|
||||
}
|
||||
|
||||
function toggleCC() {
|
||||
|
||||
@ -21,6 +21,7 @@ import EditValueModal from '@/components/Modals/EditValueModal.vue'
|
||||
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||
import { setupListActions, createToast } from '@/utils'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { capture } from '@/telemetry'
|
||||
import { call } from 'frappe-ui'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -69,6 +70,7 @@ function convertToDeal(selections, unselectAll) {
|
||||
label: __('Convert'),
|
||||
variant: 'solid',
|
||||
onClick: (close) => {
|
||||
capture('bulk_convert_to_deal')
|
||||
Array.from(selections).forEach((name) => {
|
||||
call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
||||
lead: name,
|
||||
@ -103,6 +105,7 @@ function deleteValues(selections, unselectAll) {
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
onClick: (close) => {
|
||||
capture('bulk_delete')
|
||||
call('frappe.desk.reportview.delete_items', {
|
||||
items: JSON.stringify(Array.from(selections)),
|
||||
doctype: props.doctype,
|
||||
@ -145,6 +148,7 @@ function clearAssignemnts(selections, unselectAll) {
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
onClick: (close) => {
|
||||
capture('bulk_clear_assignment')
|
||||
call('frappe.desk.form.assign_to.remove_multiple', {
|
||||
doctype: props.doctype,
|
||||
names: JSON.stringify(Array.from(selections)),
|
||||
|
||||
@ -80,6 +80,7 @@
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Tooltip, call } from 'frappe-ui'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
@ -161,6 +162,7 @@ function updateAssignees() {
|
||||
|
||||
if (addedAssignees.length) {
|
||||
if (props.docs.size) {
|
||||
capture('bulk_assign_to', { doctype: props.doctype })
|
||||
call('frappe.desk.form.assign_to.add_multiple', {
|
||||
doctype: props.doctype,
|
||||
name: JSON.stringify(Array.from(props.docs)),
|
||||
@ -171,6 +173,7 @@ function updateAssignees() {
|
||||
emit('reload')
|
||||
})
|
||||
} else {
|
||||
capture('assign_to', { doctype: props.doctype })
|
||||
call('frappe.desk.form.assign_to.add', {
|
||||
doctype: props.doctype,
|
||||
name: props.doc.name,
|
||||
|
||||
@ -92,6 +92,7 @@ import CertificateIcon from '@/components/Icons/CertificateIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { capture } from '@/telemetry'
|
||||
import { call, createResource } from 'frappe-ui'
|
||||
import { ref, nextTick, watch, computed } from 'vue'
|
||||
import { createToast } from '@/utils'
|
||||
@ -160,7 +161,10 @@ async function callInsertDoc() {
|
||||
..._contact.value,
|
||||
},
|
||||
})
|
||||
doc.name && handleContactUpdate(doc)
|
||||
if (doc.name) {
|
||||
capture('contact_created')
|
||||
handleContactUpdate(doc)
|
||||
}
|
||||
}
|
||||
|
||||
function handleContactUpdate(doc) {
|
||||
|
||||
@ -61,6 +61,7 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Switch, createResource } from 'frappe-ui'
|
||||
import { computed, ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -201,6 +202,7 @@ function createDeal() {
|
||||
isDealCreating.value = true
|
||||
},
|
||||
onSuccess(name) {
|
||||
capture('deal_created')
|
||||
isDealCreating.value = false
|
||||
show.value = false
|
||||
router.push({ name: 'Deal', params: { dealId: name } })
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
<script setup>
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import { FormControl, call, createResource, TextEditor, DatePicker } from 'frappe-ui'
|
||||
import { ref, computed, onMounted, h } from 'vue'
|
||||
|
||||
@ -115,6 +116,7 @@ function updateValues() {
|
||||
newValue.value = ''
|
||||
loading.value = false
|
||||
show.value = false
|
||||
capture('bulk_update', { doctype: props.doctype })
|
||||
emit('reload')
|
||||
})
|
||||
}
|
||||
|
||||
@ -96,6 +96,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { capture } from '@/telemetry'
|
||||
import { Checkbox, Select, TextEditor, call } from 'frappe-ui'
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
|
||||
@ -171,7 +172,10 @@ async function callInsertDoc() {
|
||||
..._emailTemplate.value,
|
||||
},
|
||||
})
|
||||
doc.name && handleEmailTemplateUpdate(doc)
|
||||
if (doc.name) {
|
||||
capture('email_template_created', { doctype: doc.reference_doctype })
|
||||
handleEmailTemplateUpdate(doc)
|
||||
}
|
||||
}
|
||||
|
||||
function handleEmailTemplateUpdate(doc) {
|
||||
|
||||
@ -46,6 +46,7 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { capture } from '@/telemetry'
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { computed, onMounted, ref, reactive, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -153,6 +154,7 @@ function createNewLead() {
|
||||
isLeadCreating.value = true
|
||||
},
|
||||
onSuccess(data) {
|
||||
capture('lead_created')
|
||||
isLeadCreating.value = false
|
||||
show.value = false
|
||||
router.push({ name: 'Lead', params: { leadId: data.name } })
|
||||
|
||||
@ -66,6 +66,7 @@
|
||||
|
||||
<script setup>
|
||||
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import { TextEditor, call } from 'frappe-ui'
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -124,6 +125,7 @@ async function updateNote() {
|
||||
},
|
||||
})
|
||||
if (d.name) {
|
||||
capture('note_created')
|
||||
notes.value?.reload()
|
||||
emit('after', d, true)
|
||||
}
|
||||
|
||||
@ -67,6 +67,7 @@ import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { formatNumberIntoCurrency } from '@/utils'
|
||||
import { capture } from '@/telemetry'
|
||||
import { call, FeatherIcon, createResource } from 'frappe-ui'
|
||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -157,7 +158,10 @@ async function callInsertDoc() {
|
||||
},
|
||||
})
|
||||
loading.value = false
|
||||
doc.name && handleOrganizationUpdate(doc)
|
||||
if (doc.name) {
|
||||
capture('organization_created')
|
||||
handleOrganizationUpdate(doc)
|
||||
}
|
||||
}
|
||||
|
||||
function handleOrganizationUpdate(doc, renamed = false) {
|
||||
|
||||
@ -118,6 +118,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { taskStatusOptions, taskPriorityOptions } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { capture } from '@/telemetry'
|
||||
import { TextEditor, Dropdown, Tooltip, call, DateTimePicker } from 'frappe-ui'
|
||||
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -199,6 +200,7 @@ async function updateTask() {
|
||||
},
|
||||
})
|
||||
if (d.name) {
|
||||
capture('task_created')
|
||||
tasks.value.reload()
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +75,6 @@ const templates = createListResource({
|
||||
filters: { status: 'APPROVED', for_doctype: ['in', [props.doctype, '']] },
|
||||
orderBy: 'modified desc',
|
||||
pageLength: 99999,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@ -18,10 +18,7 @@
|
||||
<div class="flex gap-1">
|
||||
<Tooltip :text="__('Mark all as read')">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="() => notificationsStore().mark_as_read.reload()"
|
||||
>
|
||||
<Button variant="ghost" @click="() => markAllAsRead()">
|
||||
<template #icon>
|
||||
<MarkAsDoneIcon class="h-4 w-4" />
|
||||
</template>
|
||||
@ -48,7 +45,7 @@
|
||||
:key="n.comment"
|
||||
:to="getRoute(n)"
|
||||
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
|
||||
@ -98,6 +95,7 @@ import { notificationsStore } from '@/stores/notifications'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { timeAgo } from '@/utils'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
@ -113,17 +111,23 @@ onClickOutside(
|
||||
},
|
||||
{
|
||||
ignore: ['#notifications-btn'],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function toggleNotificationPanel() {
|
||||
notificationsStore().toggle()
|
||||
}
|
||||
|
||||
function mark_as_read(doc) {
|
||||
function markAsRead(doc) {
|
||||
capture('notification_mark_as_read')
|
||||
notificationsStore().mark_doc_as_read(doc)
|
||||
}
|
||||
|
||||
function markAllAsRead() {
|
||||
capture('notification_mark_all_as_read')
|
||||
notificationsStore().mark_as_read.reload()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
$socket.off('crm_notification')
|
||||
})
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
:key="s.label"
|
||||
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">
|
||||
<Tooltip v-if="s.tooltipText" :text="__(s.tooltipText)">
|
||||
<div class="ml-2 cursor-pointer">
|
||||
@ -43,6 +45,7 @@
|
||||
import { Dropdown, Tooltip } from 'frappe-ui'
|
||||
import { timeAgo, dateFormat, formatTime, dateTooltipFormat } from '@/utils'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { capture } from '@/telemetry'
|
||||
import { computed, defineModel } from 'vue'
|
||||
|
||||
const data = defineModel()
|
||||
@ -58,8 +61,8 @@ let slaSection = computed(() => {
|
||||
data.value.sla_status == 'Failed'
|
||||
? 'red'
|
||||
: data.value.sla_status == 'Fulfilled'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
|
||||
if (status == 'First Response Due') {
|
||||
status = timeAgo(data.value.response_by)
|
||||
@ -94,11 +97,13 @@ let slaSection = computed(() => {
|
||||
options: communicationStatuses.data?.map((status) => ({
|
||||
label: status.name,
|
||||
value: status.name,
|
||||
onClick: () =>
|
||||
emit('updateField', 'communication_status', status.name),
|
||||
onClick: () => {
|
||||
capture('sla_status_change')
|
||||
emit('updateField', 'communication_status', status.name)
|
||||
},
|
||||
})),
|
||||
},
|
||||
]
|
||||
],
|
||||
)
|
||||
return sections
|
||||
})
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import QuickEntryLayoutBuilder from '@/components/Settings/QuickEntryLayoutBuilder.vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
|
||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||
|
||||
@ -122,6 +123,7 @@ function saveChanges() {
|
||||
).then(() => {
|
||||
loading.value = false
|
||||
show.value = false
|
||||
capture('quick_entry_layout_builder', { doctype: _doctype.value })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -74,6 +74,7 @@ import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayoutBuilder from '@/components/Settings/SidePanelLayoutBuilder.vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
|
||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||
|
||||
@ -143,6 +144,7 @@ function saveChanges() {
|
||||
).then(() => {
|
||||
loading.value = false
|
||||
show.value = false
|
||||
capture('side_panel_layout_builder', { doctype: _doctype.value })
|
||||
emit('reload')
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
import './index.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import {
|
||||
FrappeUI,
|
||||
Button,
|
||||
@ -19,9 +14,14 @@ import {
|
||||
frappeRequest,
|
||||
FeatherIcon,
|
||||
} from 'frappe-ui'
|
||||
import translationPlugin from './translation'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createDialog } from './utils/dialogs'
|
||||
import { initSocket } from './socket'
|
||||
import router from './router'
|
||||
import translationPlugin from './translation'
|
||||
import { posthogPlugin } from './telemetry'
|
||||
import App from './App.vue'
|
||||
|
||||
let globalComponents = {
|
||||
Button,
|
||||
@ -45,6 +45,7 @@ app.use(FrappeUI)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(translationPlugin)
|
||||
app.use(posthogPlugin)
|
||||
for (let key in globalComponents) {
|
||||
app.component(key, globalComponents[key])
|
||||
}
|
||||
@ -61,7 +62,7 @@ if (import.meta.env.DEV) {
|
||||
socket = initSocket()
|
||||
app.config.globalProperties.$socket = socket
|
||||
app.mount('#app')
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
socket = initSocket()
|
||||
|
||||
@ -312,6 +312,7 @@ import { organizationsStore } from '@/stores/organizations'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||
import { capture } from '@/telemetry'
|
||||
import {
|
||||
createResource,
|
||||
FileUploader,
|
||||
@ -587,6 +588,7 @@ async function convertToDeal(updated) {
|
||||
},
|
||||
)
|
||||
if (deal) {
|
||||
capture('convert_lead_to_deal')
|
||||
if (updated) {
|
||||
await organizations.reload()
|
||||
await contacts.reload()
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import { defineStore } from 'pinia'
|
||||
import { createListResource } from 'frappe-ui'
|
||||
import { reactive, h } from 'vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
|
||||
export const statusesStore = defineStore('crm-statuses', () => {
|
||||
let leadStatusesByName = reactive({})
|
||||
@ -103,6 +104,7 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
||||
class: statusesByName[status].iconColorClass,
|
||||
}),
|
||||
onClick: () => {
|
||||
capture('status_changed', { doctype, status })
|
||||
action && action('status', statusesByName[status].name)
|
||||
},
|
||||
})
|
||||
|
||||
97
frontend/src/telemetry.ts
Normal file
97
frontend/src/telemetry.ts
Normal 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,
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user