session-tab-switch-metrics.ts 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. export type SessionSwitchSample = {
  2. observedAtMs: number
  3. destination: string[]
  4. source: string[]
  5. hasVisibleRows: boolean
  6. last: boolean
  7. requiredPartVisible?: boolean
  8. bottomAnchorRequired?: boolean
  9. bottomErrorPx?: number
  10. review?: {
  11. fileHost: boolean
  12. fileHostReplaced: boolean
  13. header: string
  14. replacedLevels: string[]
  15. }
  16. }
  17. export function classifySessionSwitch(samples: SessionSwitchSample[]) {
  18. const firstDestination = samples.findIndex((sample) => sample.destination.length > 0)
  19. const firstCorrect = samples.findIndex(isCorrectDestination)
  20. const stable = samples.findIndex((_, index) => isStableSessionSwitch(samples.slice(index, index + 3)))
  21. return {
  22. firstDestinationObservedMs: samples[firstDestination]?.observedAtMs ?? null,
  23. firstCorrectObservedMs: samples[firstCorrect]?.observedAtMs ?? null,
  24. stableObservedMs: samples[stable + 2]?.observedAtMs ?? null,
  25. wrongDestinationSamples: samples
  26. .slice(firstDestination)
  27. .filter((sample) => sample.destination.length > 0 && !sample.last).length,
  28. blankSamples: samples.filter((sample) => !sample.hasVisibleRows).length,
  29. unknownSamples: samples.filter(
  30. (sample) => sample.hasVisibleRows && sample.destination.length === 0 && sample.source.length === 0,
  31. ).length,
  32. sourceSamples: samples.filter((sample) => sample.source.length > 0).length,
  33. reviewFileHostMissingSamples: samples.filter((sample) => sample.review && !sample.review.fileHost).length,
  34. reviewFileHostReplacedSamples: samples.filter((sample) => sample.review?.fileHostReplaced).length,
  35. reviewHeaders: [...new Set(samples.flatMap((sample) => (sample.review ? [sample.review.header] : [])))],
  36. reviewReplacedLevels: [...new Set(samples.flatMap((sample) => sample.review?.replacedLevels ?? []))],
  37. }
  38. }
  39. export function isCorrectDestination(sample: SessionSwitchSample) {
  40. return (
  41. sample.destination.length > 0 &&
  42. sample.source.length === 0 &&
  43. sample.last &&
  44. sample.requiredPartVisible !== false &&
  45. (sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1)
  46. )
  47. }
  48. export function isStableSessionSwitch(samples: SessionSwitchSample[]) {
  49. return samples.length === 3 && samples.every(isCorrectDestination)
  50. }
  51. export function isStableDestination(samples: Pick<SessionSwitchSample, "last" | "bottomErrorPx">[]) {
  52. return (
  53. samples.length === 3 && samples.every((sample) => sample.last && Math.abs(sample.bottomErrorPx ?? Infinity) <= 1)
  54. )
  55. }