use-editing-session-heartbeat.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  2. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  3. import { EditorType } from '@/features/ide-react/editor/types/editor-type'
  4. import { reportCM6Perf } from '@/infrastructure/cm6-performance'
  5. import { putJSON } from '@/infrastructure/fetch-json'
  6. import { debugConsole } from '@/utils/debugging'
  7. import moment from 'moment'
  8. import { useCallback, useState } from 'react'
  9. import useEventListener from '@/shared/hooks/use-event-listener'
  10. import useDomEventListener from '@/shared/hooks/use-dom-event-listener'
  11. function createEditingSessionHeartbeatData(editorType: EditorType) {
  12. const segmentation: Record<string, unknown> = {
  13. editorType,
  14. }
  15. const cm6PerfData = reportCM6Perf()
  16. // Ignore if no typing has happened
  17. if (cm6PerfData.numberOfEntries > 0) {
  18. for (const [key, value] of Object.entries(cm6PerfData)) {
  19. const segmentationPropName =
  20. 'cm6Perf' + key.charAt(0).toUpperCase() + key.slice(1)
  21. if (value !== null) {
  22. segmentation[segmentationPropName] = value
  23. }
  24. }
  25. }
  26. return segmentation
  27. }
  28. function sendEditingSessionHeartbeat(
  29. projectId: string,
  30. segmentation: Record<string, unknown>
  31. ) {
  32. putJSON(`/editingSession/${projectId}`, {
  33. body: { segmentation },
  34. }).catch(debugConsole.error)
  35. }
  36. export function useEditingSessionHeartbeat() {
  37. const { projectId } = useIdeReactContext()
  38. const { getEditorType } = useEditorManagerContext()
  39. // Keep track of how many heartbeats we've sent so that we can calculate how
  40. // long wait until the next one
  41. const [heartbeatsSent, setHeartbeatsSent] = useState(0)
  42. const [nextHeartbeatAt, setNextHeartbeatAt] = useState(() => new Date())
  43. const editingSessionHeartbeat = useCallback(() => {
  44. debugConsole.log('[Event] heartbeat trigger')
  45. const editorType = getEditorType()
  46. if (editorType === null) return
  47. // If the next heartbeat is in the future, stop
  48. if (nextHeartbeatAt > new Date()) return
  49. const segmentation = createEditingSessionHeartbeatData(editorType)
  50. debugConsole.log('[Event] send heartbeat request', segmentation)
  51. sendEditingSessionHeartbeat(projectId, segmentation)
  52. setHeartbeatsSent(heartbeatsSent => heartbeatsSent + 1)
  53. // Send two first heartbeats at 0 and 30s then increase the backoff time
  54. // 1min per call until we reach 5 min
  55. const backoffSecs =
  56. heartbeatsSent <= 2
  57. ? 30
  58. : heartbeatsSent <= 6
  59. ? (heartbeatsSent - 2) * 60
  60. : 300
  61. setNextHeartbeatAt(moment().add(backoffSecs, 'seconds').toDate())
  62. }, [getEditorType, heartbeatsSent, nextHeartbeatAt, projectId])
  63. // Hook the heartbeat up to editor events
  64. useEventListener('cursor:editor:update', editingSessionHeartbeat)
  65. useEventListener('scroll:editor:update', editingSessionHeartbeat)
  66. useDomEventListener(document, 'click', editingSessionHeartbeat)
  67. }