Skip to content

Commit

Permalink
Additional metrics (#609)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
MelissaAutumn authored Aug 7, 2024
1 parent d5e4979 commit eed8c67
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 53 deletions.
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 43 additions & 4 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,23 +188,46 @@ provide(refreshKey, getDbData);
onMounted(async () => {
if (usePosthog) {
const REMOVED_PROPERTY = '<removed>';
const UNKNOWN_PROPERTY = '<unknown>';
// 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,
person_profiles: 'identified_only',
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 ?? '<unknown path to mask>';
// 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!
Expand All @@ -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;
},
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</span>
<span v-else-if="fieldData.type === TableDataType.Code" class="flex items-center gap-4">
<code>{{ fieldData.value }}</code>
<text-button class="btn-copy" :copy="String(fieldData.value)" :title="t('label.copy')" />
<text-button :uid="fieldKey" class="btn-copy" :copy="String(fieldData.value)" :title="t('label.copy')" />
</span>
<span v-else-if="fieldData.type === TableDataType.Bool">
<span v-if="fieldData.value">Yes</span>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const logout = async () => {
</router-link>
<text-button
v-show="user.myLink"
uid="myLink"
:label="t('label.shareMyLink')"
:copy="user.myLink"
:title="t('label.copy')"
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/components/ScheduleCreation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -392,18 +392,22 @@ import SecondaryButton from '@/elements/SecondaryButton';
// icons
import { IconChevronDown, IconInfoCircle } from '@tabler/icons-vue';
import AlertBox from '@/elements/AlertBox';
import SwitchToggle from '@/elements/SwitchToggle';
import ToolTip from '@/elements/ToolTip.vue';
import { useCalendarStore } from '@/stores/calendar-store';
import { useExternalConnectionsStore } from '@/stores/external-connections-store';
import SnackishBar from '@/elements/SnackishBar.vue';
import { dayjsKey } from '@/keys';
import { useScheduleStore } from '@/stores/schedule-store';
// component constants
const user = useUserStore();
const calendarStore = useCalendarStore();
const externalConnectionStore = useExternalConnectionsStore();
const scheduleStore = useScheduleStore();
const { t } = useI18n();
const dj = inject(dayjsKey);
const call = inject('call');
Expand Down Expand Up @@ -656,19 +660,22 @@ const saveSchedule = async (withConfirmation = true) => {
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;
Expand Down
35 changes: 20 additions & 15 deletions frontend/src/components/SettingsAccount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
};
Expand All @@ -119,6 +133,8 @@ const refreshLinkConfirm = async () => {
await user.changeSignedUrl(call);
await refreshData();
closeModals();
sendMetrics(MetricEvents.RefreshLink);
};
/**
Expand All @@ -136,6 +152,7 @@ const actuallyDownloadData = async () => {
window.location.assign(fileObj);
closeModals();
sendMetrics(MetricEvents.DownloadData);
};
/**
Expand All @@ -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);
Expand Down Expand Up @@ -220,6 +239,7 @@ const actuallyDeleteAccount = async () => {
</a>
</div>
<text-button
uid="myLink"
class="btn-copy"
:tooltip="t('label.copyLink')"
:copy="user.myLink"
Expand Down Expand Up @@ -307,18 +327,3 @@ const actuallyDeleteAccount = async () => {
@close="closeModals"
></confirmation-modal>
</template>

<style scoped>
/* If the device does not support hover (i.e. mobile) then make it activate on focus within */
@media (hover: none) {
.tooltip-label:focus-within .tooltip {
display: block;
}
}
.tooltip-icon:hover ~ .tooltip {
display: block;
}
</style>
33 changes: 30 additions & 3 deletions frontend/src/components/SettingsCalendar.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { CalendarManagementType, CalendarProviders } from '@/definitions';
import { CalendarManagementType, CalendarProviders, MetricEvents } from '@/definitions';
import { IconArrowRight } from '@tabler/icons-vue';
import {
ref, reactive, inject, onMounted, computed,
Expand All @@ -16,6 +16,7 @@ import ConfirmationModal from '@/components/ConfirmationModal.vue';
import GoogleCalendarButton from '@/elements/GoogleCalendarButton.vue';
import PrimaryButton from '@/elements/PrimaryButton.vue';
import SecondaryButton from '@/elements/SecondaryButton.vue';
import { posthog, usePosthog } from '@/composables/posthog';
// component constants
const { t } = useI18n({ useScope: 'global' });
Expand Down Expand Up @@ -77,6 +78,17 @@ const resetInput = () => {
loading.value = false;
};
/**
* Send off some metrics
* @param event {MetricEvents}
* @param properties {Object}
*/
const sendMetrics = (event, properties = {}) => {
if (usePosthog) {
posthog.capture(event, properties);
}
};
// set input mode for adding or editing
const addCalendar = (provider: number) => {
inputMode.value = InputModes.Add;
Expand All @@ -88,19 +100,27 @@ const connectCalendar = async (id: number) => {
await calendarStore.connectCalendar(call, id);
await refreshData();
resetInput();
sendMetrics(MetricEvents.ConnectCalendar, { provider: calendarStore.calendarById(id)?.provider });
};
const disconnectCalendar = async (id: number) => {
loading.value = true;
await calendarStore.disconnectCalendar(call, id);
await refreshData();
resetInput();
sendMetrics(MetricEvents.DisconnectCalendar, { provider: calendarStore.calendarById(id)?.provider });
};
const syncCalendars = async () => {
loading.value = true;
loading.value = true;
const oldCount = calendarStore.calendars.value.length;
await calendarStore.syncCalendars(call);
await refreshData();
const newCount = calendarStore.calendars.value.length;
sendMetrics(MetricEvents.DisconnectCalendar, { oldCount, newCount });
};
const editCalendar = async (id: number) => {
loading.value = true;
Expand Down Expand Up @@ -133,6 +153,10 @@ const deleteCalendarConfirm = async () => {
const saveCalendar = async () => {
loading.value = true;
if (inputMode.value === inputModes.add) {
sendMetrics(MetricEvents.AddCalendar, { provider: calendarInput.data.provider });
}
// add new caldav calendar
if (isCalDav.value && inputMode.value === InputModes.Add) {
const { error, data }: CalendarResponse = await call('cal').post(calendarInput.data).json();
Expand All @@ -149,10 +173,13 @@ const saveCalendar = async () => {
await calendarStore.connectGoogleCalendar(call, calendarInput.data.user);
return;
}
// edit existing calendar connection
if (inputMode.value === InputModes.Edit) {
if (inputMode.value === inputModes.edit) {
await call(`cal/${calendarInput.id}`).put(calendarInput.data);
sendMetrics(MetricEvents.EditCalendar, { provider: calendarInput.data.provider });
}
// refresh list of calendars
await refreshData();
resetInput();
Expand Down
Loading

0 comments on commit eed8c67

Please sign in to comment.