diff --git a/src/libs/external-telemetry/event-tracking.ts b/src/libs/external-telemetry/event-tracking.ts new file mode 100644 index 000000000..bb8ac811f --- /dev/null +++ b/src/libs/external-telemetry/event-tracking.ts @@ -0,0 +1,35 @@ +import posthog from 'posthog-js' + +/** + * PostHog client + */ +class PostHog { + static posthog: ReturnType | undefined = undefined + + /** + * Initialize the PostHog client if not already initialized + */ + constructor() { + if (!PostHog.posthog) { + PostHog.posthog = posthog.init('phc_SfqVeZcpYHmhUn9NRizThxFxiI9fKqvjRjmBDB8ToRs', { + api_host: 'https://us.i.posthog.com', + person_profiles: 'always', // Create profiles for anonymous users as well + }) + } + } + + /** + * Capture an event + * @param {string} eventName - The name of the event + * @param {Record} properties - The properties of the event + */ + capture(eventName: string, properties?: Record): void { + if (!PostHog.posthog) { + throw new Error('PostHog client not initialized.') + } + PostHog.posthog.capture(eventName, properties) + } +} + +const eventTracker = new PostHog() +export default eventTracker diff --git a/src/main.ts b/src/main.ts index e6a638250..007fef33a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import { createApp } from 'vue' import VueVirtualScroller from 'vue-virtual-scroller' import { app_version } from '@/libs/cosmos' +import eventTracker from '@/libs/external-telemetry/event-tracking' import App from './App.vue' import vuetify from './plugins/vuetify' @@ -25,6 +26,8 @@ loadFonts() const app = createApp(App) +eventTracker.capture('App started') + // Initialize Sentry for error tracking // Only track usage statistics if the user has not opted out and the app is not in development mode if (window.localStorage.getItem('cockpit-enable-usage-statistics-telemetry') && import.meta.env.DEV === false) { diff --git a/src/stores/mainVehicle.ts b/src/stores/mainVehicle.ts index b8617c5df..19f9b2bf2 100644 --- a/src/stores/mainVehicle.ts +++ b/src/stores/mainVehicle.ts @@ -1,4 +1,5 @@ import { useStorage, useTimestamp, watchThrottled } from '@vueuse/core' +import { differenceInSeconds } from 'date-fns' import { defineStore } from 'pinia' import { v4 as uuid } from 'uuid' import { computed, reactive, ref, watch } from 'vue' @@ -18,6 +19,7 @@ import { ConnectionManager } from '@/libs/connection/connection-manager' import type { Package } from '@/libs/connection/m2r/messages/mavlink2rest' import { MavAutopilot, MAVLinkType, MavType } from '@/libs/connection/m2r/messages/mavlink2rest-enum' import type { Message } from '@/libs/connection/m2r/messages/mavlink2rest-message' +import eventTracker from '@/libs/external-telemetry/event-tracking' import { availableCockpitActions, registerActionCallback } from '@/libs/joystick/protocols/cockpit-actions' import { MavlinkManualControlManager } from '@/libs/joystick/protocols/mavlink-manual-control' import type { ArduPilot } from '@/libs/vehicle/ardupilot/ardupilot' @@ -121,6 +123,7 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { const genericVariables: Record = reactive({}) const availableGenericVariables = ref([]) const usedGenericVariables = ref([]) + const vehicleArmingTime = ref(undefined) const mode = ref(undefined) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -387,7 +390,19 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { Object.assign(attitude, newAttitude) }) mainVehicle.value.onArm.add((armed: boolean) => { + const wasArmed = isArmed.value isArmed.value = armed + + // If the vehicle was already in the desired state or it's the first time we are checking, do not capture an event + if (wasArmed === undefined || wasArmed === armed) return + + if (armed) { + vehicleArmingTime.value = new Date() + eventTracker.capture('Vehicle armed') + } else { + const armDurationInSeconds = differenceInSeconds(new Date(), vehicleArmingTime.value ?? new Date()) + eventTracker.capture('Vehicle disarmed', { armDurationInSeconds }) + } }) mainVehicle.value.onTakeoff.add((inAir: boolean) => { flying.value = inAir diff --git a/src/stores/omniscientLogger.ts b/src/stores/omniscientLogger.ts index eb087cef4..119b4f60f 100644 --- a/src/stores/omniscientLogger.ts +++ b/src/stores/omniscientLogger.ts @@ -1,5 +1,6 @@ import { WebRTCStats } from '@peermetrics/webrtc-stats' import { useDocumentVisibility } from '@vueuse/core' +import { differenceInSeconds } from 'date-fns' import { defineStore } from 'pinia' import { ref, watch } from 'vue' @@ -8,6 +9,7 @@ import { createCockpitActionVariable, setCockpitActionVariableData, } from '@/libs/actions/data-lake' +import eventTracker from '@/libs/external-telemetry/event-tracking' import { WebRTCStatsEvent, WebRTCVideoStat } from '@/types/video' import { useVideoStore } from './video' @@ -275,6 +277,12 @@ export const useOmniscientLoggerStore = defineStore('omniscient-logger', () => { } }) + // Routine to send a ping event to the event tracking system every 5 minutes + const initialTimestamp = new Date() + setInterval(() => { + eventTracker.capture('Ping', { runningTimeInSeconds: differenceInSeconds(new Date(), initialTimestamp) }) + }, 1000 * 60 * 5) + return { streamsFrameRateHistory, appFrameRateHistory, diff --git a/src/stores/video.ts b/src/stores/video.ts index f6bb3900f..34d53ff4b 100644 --- a/src/stores/video.ts +++ b/src/stores/video.ts @@ -14,6 +14,7 @@ import { useBlueOsStorage } from '@/composables/settingsSyncer' import { useSnackbar } from '@/composables/snackbar' import { WebRTCManager } from '@/composables/webRTC' import { getIpsInformationFromVehicle } from '@/libs/blueos' +import eventTracker from '@/libs/external-telemetry/event-tracking' import { availableCockpitActions, registerActionCallback } from '@/libs/joystick/protocols/cockpit-actions' import { datalogger } from '@/libs/sensors-logging' import { isEqual, sleep } from '@/libs/utils' @@ -190,6 +191,10 @@ export const useVideoStore = defineStore('video', () => { const stopRecording = (streamName: string): void => { if (activeStreams.value[streamName] === undefined) activateStream(streamName) + const timeRecordingStart = activeStreams.value[streamName]?.timeRecordingStart + const durationInSeconds = timeRecordingStart ? differenceInSeconds(new Date(), timeRecordingStart) : undefined + eventTracker.capture('Video recording stop', { streamName, durationInSeconds }) + activeStreams.value[streamName]!.timeRecordingStart = undefined activeStreams.value[streamName]!.mediaRecorder!.stop() @@ -249,6 +254,7 @@ export const useVideoStore = defineStore('video', () => { * @param {string} streamName - Name of the stream */ const startRecording = async (streamName: string): Promise => { + eventTracker.capture('Video recording start', { streamName: streamName }) if (activeStreams.value[streamName] === undefined) activateStream(streamName) if (namesAvailableStreams.value.isEmpty()) {