From eed8c671ec98e79cac30da754abf5094b8a3ac2d Mon Sep 17 00:00:00 2001 From: Mel <97147377+MelissaAutumn@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:23:07 -0700 Subject: [PATCH] Additional metrics (#609) * Adds the following metrics to help guide development resources and priority: * apmt.copy - Copy to clipboard with a generic uid and label of the button * apmt.calendar.add - Add a calendar with provider * apmt.calendar.edit - Edit a calendar with provider * apmt.calendar.delete - Delete a calendar with provider * apmt.calendar.connect - Connect a calendar with provider * apmt.calendar.disconnect - Disconnect a calendar with provider * apmt.calendar.sync - Sync your calendars with old and new calendar counts * apmt.account.refreshLink - Refresh your booking link * apmt.account.download - Download your account data * apmt.account.deleteAccount - Delete your account * Add some additional metrics: * apmt.booking.confirm * apmt.booking.deny * apmt.booking.request * apmt.schedule.created * apmt.schedule.updated * Temp patch a posthog function to prevent sanitize routes * Remove a debug call * Protect against Vue's HMR * Remove web vital current_url * Come on vuejs. --- frontend/package.json | 2 +- frontend/src/App.vue | 47 +++++++++++++++++-- frontend/src/components/DataTable.vue | 2 +- frontend/src/components/NavBar.vue | 1 + frontend/src/components/ScheduleCreation.vue | 17 +++++-- frontend/src/components/SettingsAccount.vue | 35 ++++++++------ frontend/src/components/SettingsCalendar.vue | 33 +++++++++++-- frontend/src/definitions.ts | 41 ++++++++++++---- frontend/src/elements/TextButton.vue | 12 ++++- frontend/src/stores/calendar-store.ts | 19 +++++++- frontend/src/stores/schedule-store.ts | 15 ++++-- .../src/views/BookingConfirmationView.vue | 15 ++++-- frontend/src/views/BookingView.vue | 15 ++++-- frontend/src/views/SettingsView.vue | 5 +- 14 files changed, 206 insertions(+), 53 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index ac8cea140..596e11d44 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,7 @@ "core-js": "^3.8.3", "dayjs": "^1.11.5", "pinia": "^2.1.6", - "posthog-js": "^1.150.1", + "posthog-js": "^1.154.5", "qalendar": "^3.7.0", "tailwindcss": "^3.4.3", "ua-parser-js": "^1.0.38", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d38b4dae6..608cc6b3f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -188,6 +188,25 @@ provide(refreshKey, getDbData); onMounted(async () => { if (usePosthog) { + const REMOVED_PROPERTY = ''; + const UNKNOWN_PROPERTY = ''; + + // Hack to clear $set_once until we get confirmation that this can be filtered. + // Move the function reference so we can patch it and still retrieve the results before we sanitize it. + if (posthog['_original_calculate_set_once_properties'] === undefined) { + posthog['_original_calculate_set_once_properties'] = posthog._calculate_set_once_properties; + } + posthog._calculate_set_once_properties = function (dataSetOnce?) { + dataSetOnce = posthog['_original_calculate_set_once_properties'](dataSetOnce); + + if (dataSetOnce?.$initial_current_url || dataSetOnce?.$initial_pathname) { + dataSetOnce.$initial_current_url = REMOVED_PROPERTY; + dataSetOnce.$initial_pathname = REMOVED_PROPERTY; + } + + return dataSetOnce; + }; + posthog.init(import.meta.env.VITE_POSTHOG_PROJECT_KEY, { api_host: import.meta.env.VITE_POSTHOG_HOST, ui_host: import.meta.env.VITE_POSTHOG_UI_HOST, @@ -195,16 +214,20 @@ onMounted(async () => { persistence: 'memory', mask_all_text: true, mask_all_element_attributes: true, + autocapture: false, // Off for now until we can figure out $set_once. sanitize_properties: (properties, event) => { // If the route isn't available to use right now, ignore the capture. if (!route.name) { - return {}; + return { + captureFailedMessage: 'route.name was not available.', + }; } // Do we need to mask the path? if (route.meta?.maskForMetrics) { // Replace recorded path with the path definition - const vuePath = route.matched[0]?.path ?? ''; + // So basically: /user/melissaa/dfb0d2aa/ -> /user/:username/:signatureOrSlug + const vuePath = route.matched[0]?.path ?? UNKNOWN_PROPERTY; const oldPath = properties.$pathname; // Easiest just to string replace all instances! @@ -217,10 +240,26 @@ onMounted(async () => { } if (event === '$pageleave') { - // We don't have access to the previous route, so just null it out. - properties.$prev_pageview_pathname = null; + // FIXME: Removed pending matching with vue routes + properties.$prev_pageview_pathname = REMOVED_PROPERTY; + } + + // Remove initial properties + if (!properties.$set) { + properties.$set = {}; } + properties.$set.$initial_current_url = REMOVED_PROPERTY; + properties.$set.$initial_pathname = REMOVED_PROPERTY; + + // Clean up webvitals + // Ref: https://github.com/PostHog/posthog-js/blob/f5a0d12603197deab305a7e25843f04f3fa4c99e/src/extensions/web-vitals/index.ts#L175 + ['LCP', 'CLS', 'FCP', 'INP'].forEach((metric) => { + if (properties[`$web_vitals_${metric}_event`]?.$current_url) { + properties[`$web_vitals_${metric}_event`].$current_url = REMOVED_PROPERTY; + } + }); + return properties; }, }); diff --git a/frontend/src/components/DataTable.vue b/frontend/src/components/DataTable.vue index c114eebe9..ab3587f6c 100644 --- a/frontend/src/components/DataTable.vue +++ b/frontend/src/components/DataTable.vue @@ -41,7 +41,7 @@ {{ fieldData.value }} - + Yes diff --git a/frontend/src/components/NavBar.vue b/frontend/src/components/NavBar.vue index 40d33ad9a..627a91b88 100644 --- a/frontend/src/components/NavBar.vue +++ b/frontend/src/components/NavBar.vue @@ -77,6 +77,7 @@ const logout = async () => { { delete obj.id; // save schedule data - const { data, error } = props.schedule - ? await call(`schedule/${props.schedule.id}`).put(obj).json() - : await call('schedule/').post(obj).json(); + const response = props.schedule + ? await scheduleStore.updateSchedule(call, props.schedule.id, obj) + : await scheduleStore.createSchedule(call, obj); - if (error.value) { + if (response.error) { // error message is in data - handleErrorResponse(data); + scheduleCreationError.value = response.message; // go back to the start savingInProgress.value = false; window.scrollTo(0, 0); return; } + // Otherwise it's just data! + const { data } = response; + if (withConfirmation) { // show confirmation savedConfirmation.title = data.value.name; diff --git a/frontend/src/components/SettingsAccount.vue b/frontend/src/components/SettingsAccount.vue index 25434a08a..70df0d4d6 100644 --- a/frontend/src/components/SettingsAccount.vue +++ b/frontend/src/components/SettingsAccount.vue @@ -21,6 +21,9 @@ import { IconExternalLink, IconInfoCircle } from '@tabler/icons-vue'; import { useExternalConnectionsStore } from '@/stores/external-connections-store'; import { useScheduleStore } from '@/stores/schedule-store'; +import { MetricEvents } from '@/definitions'; +import { usePosthog, posthog } from '@/composables/posthog'; + // component constants const { t } = useI18n({ useScope: 'global' }); const call = inject(callKey); @@ -103,6 +106,17 @@ onMounted(async () => { await refreshData(); }); +/** + * Send off some metrics + * @param event {MetricEvents} + * @param properties {Object} + */ +const sendMetrics = (event, properties = {}) => { + if (usePosthog) { + posthog.capture(event, properties); + } +}; + const downloadData = async () => { downloadAccountModalOpen.value = true; }; @@ -119,6 +133,8 @@ const refreshLinkConfirm = async () => { await user.changeSignedUrl(call); await refreshData(); closeModals(); + + sendMetrics(MetricEvents.RefreshLink); }; /** @@ -136,6 +152,7 @@ const actuallyDownloadData = async () => { window.location.assign(fileObj); closeModals(); + sendMetrics(MetricEvents.DownloadData); }; /** @@ -146,6 +163,8 @@ const actuallyDeleteAccount = async () => { const { error }: BooleanResponse = await call('account/delete').delete(); + sendMetrics(MetricEvents.DeleteAccount); + if (error.value) { // TODO: show error // console.warn('ERROR: ', error.value); @@ -220,6 +239,7 @@ const actuallyDeleteAccount = async () => { { @close="closeModals" > - - diff --git a/frontend/src/components/SettingsCalendar.vue b/frontend/src/components/SettingsCalendar.vue index 94f690ed0..8cc84e660 100644 --- a/frontend/src/components/SettingsCalendar.vue +++ b/frontend/src/components/SettingsCalendar.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/stores/calendar-store.ts b/frontend/src/stores/calendar-store.ts index eee2b0e6f..146d01c31 100644 --- a/frontend/src/stores/calendar-store.ts +++ b/frontend/src/stores/calendar-store.ts @@ -20,6 +20,12 @@ export const useCalendarStore = defineStore('calendars', () => { window.location.href = googleUrl.data.value.slice(1, -1); }; + /** + * Retrieve the calendar object by id + * @param id + */ + const calendarById = (id: number) => calendars.value.filter((calendar) => calendar.id === id)?.at(0) ?? null; + /** * Get all calendars for current user * @param call preconfigured API fetch function @@ -57,6 +63,17 @@ export const useCalendarStore = defineStore('calendars', () => { }; return { - isLoaded, hasConnectedCalendars, calendars, unconnectedCalendars, connectedCalendars, fetch, $reset, connectGoogleCalendar, connectCalendar, disconnectCalendar, syncCalendars, + isLoaded, + hasConnectedCalendars, + calendars, + unconnectedCalendars, + connectedCalendars, + fetch, + $reset, + connectGoogleCalendar, + connectCalendar, + disconnectCalendar, + syncCalendars, + calendarById, }; }); diff --git a/frontend/src/stores/schedule-store.ts b/frontend/src/stores/schedule-store.ts index 2371495de..b6975e56e 100644 --- a/frontend/src/stores/schedule-store.ts +++ b/frontend/src/stores/schedule-store.ts @@ -2,9 +2,10 @@ import { i18n } from '@/composables/i18n'; import { defineStore } from 'pinia'; import { ref, computed, inject } from 'vue'; import { useUserStore } from '@/stores/user-store'; -import { dateFormatStrings } from '@/definitions'; +import { dateFormatStrings, MetricEvents } from '@/definitions'; import { Fetch, Schedule, ScheduleListResponse } from '@/models'; import { dayjsKey } from '@/keys'; +import { posthog, usePosthog } from '@/composables/posthog'; // eslint-disable-next-line import/prefer-default-export export const useScheduleStore = defineStore('schedules', () => { @@ -89,7 +90,7 @@ export const useScheduleStore = defineStore('schedules', () => { return i18n.t('error.unknownScheduleError'); }; - const createSchedule = async (call, scheduleData) => { + const createSchedule = async (call: Fetch, scheduleData: object) => { // save schedule data const { data, error } = await call('schedule/').post(scheduleData).json(); @@ -103,10 +104,14 @@ export const useScheduleStore = defineStore('schedules', () => { // Update the schedule await fetch(call, true); + if (usePosthog) { + posthog.capture(MetricEvents.ScheduleCreated); + } + return data; }; - const updateSchedule = async (call, id, scheduleData) => { + const updateSchedule = async (call: Fetch, id: number, scheduleData: object) => { // save schedule data const { data, error } = await call(`schedule/${id}`).put(scheduleData).json(); @@ -120,6 +125,10 @@ export const useScheduleStore = defineStore('schedules', () => { // Update the schedule await fetch(call, true); + if (usePosthog) { + posthog.capture(MetricEvents.ScheduleUpdated); + } + return data; }; diff --git a/frontend/src/views/BookingConfirmationView.vue b/frontend/src/views/BookingConfirmationView.vue index 1ccc602ce..efc4fb7f2 100644 --- a/frontend/src/views/BookingConfirmationView.vue +++ b/frontend/src/views/BookingConfirmationView.vue @@ -7,6 +7,8 @@ import { AvailabilitySlotResponse } from '@/models'; import ArtInvalidLink from '@/elements/arts/ArtInvalidLink.vue'; import ArtSuccessfulBooking from '@/elements/arts/ArtSuccessfulBooking.vue'; import LoadingSpinner from '@/elements/LoadingSpinner.vue'; +import { usePosthog, posthog } from '@/composables/posthog'; +import { MetricEvents } from '@/definitions'; const { t } = useI18n(); const route = useRoute(); @@ -33,9 +35,16 @@ onMounted(async () => { const { error, data }: AvailabilitySlotResponse = await call('schedule/public/availability/booking').put(obj).json(); if (error.value) { isError.value = true; - } else { - isError.value = false; - attendeeEmail.value = data.value?.attendee?.email; + + return; + } + + isError.value = false; + attendeeEmail.value = data.value?.attendee?.email; + + if (usePosthog) { + const event = confirmed ? MetricEvents.ConfirmBooking : MetricEvents.DenyBooking; + posthog.capture(event); } }); diff --git a/frontend/src/views/BookingView.vue b/frontend/src/views/BookingView.vue index 5a5d04972..f624985ed 100644 --- a/frontend/src/views/BookingView.vue +++ b/frontend/src/views/BookingView.vue @@ -1,17 +1,20 @@