EdgeFunctionOverview.utils.test.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import dayjs from 'dayjs'
  2. import { describe, expect, it } from 'vitest'
  3. import {
  4. EDGE_FUNCTION_CHART_INTERVALS,
  5. formatChartTimestamp,
  6. formatMetric,
  7. formatRate,
  8. formatReferenceDelta,
  9. getBucketedTimeRange,
  10. getChartEmptyStateCopy,
  11. getChartTimeRangeLabels,
  12. getExecutionMetrics,
  13. getInvocationChartData,
  14. getInvocationTotals,
  15. getInvocationUpdateAnnotation,
  16. getMemoryTooltipDetail,
  17. getRollingTimeRange,
  18. getSegmentedButtonClassName,
  19. getUsageMetrics,
  20. toEdgeFunctionChartData,
  21. type EdgeFunctionChartRawDatum,
  22. } from './EdgeFunctionOverview.utils'
  23. describe('EdgeFunctionOverview.utils', () => {
  24. it('uses a full day interval for the 1 day option', () => {
  25. expect(
  26. EDGE_FUNCTION_CHART_INTERVALS.find((interval) => interval.key === '1day')?.startUnit
  27. ).toBe('day')
  28. })
  29. it('builds invocation chart data and totals from combined stats', () => {
  30. const chartData = toEdgeFunctionChartData([
  31. {
  32. timestamp: '2026-03-20T10:00:00.000Z',
  33. success_count: 10,
  34. redirect_count: 2,
  35. client_err_count: 1,
  36. server_err_count: 3,
  37. },
  38. {
  39. timestamp: '2026-03-20T10:15:00.000Z',
  40. success_count: '4',
  41. redirect_count: '1',
  42. client_err_count: '0',
  43. server_err_count: '2',
  44. },
  45. ] satisfies EdgeFunctionChartRawDatum[])
  46. const data = getInvocationChartData(chartData)
  47. expect(data).toEqual([
  48. {
  49. timestamp: '2026-03-20T10:00:00.000Z',
  50. ok_count: 10,
  51. warning_count: 3,
  52. error_count: 3,
  53. },
  54. {
  55. timestamp: '2026-03-20T10:15:00.000Z',
  56. ok_count: 4,
  57. warning_count: 1,
  58. error_count: 2,
  59. },
  60. ])
  61. expect(chartData[1]).toEqual({
  62. timestamp: '2026-03-20T10:15:00.000Z',
  63. success_count: 4,
  64. redirect_count: 1,
  65. client_err_count: 0,
  66. server_err_count: 2,
  67. avg_execution_time: 0,
  68. max_execution_time: 0,
  69. avg_cpu_time_used: 0,
  70. max_cpu_time_used: 0,
  71. avg_memory_used: 0,
  72. avg_heap_memory_used: 0,
  73. avg_external_memory_used: 0,
  74. })
  75. expect(getInvocationTotals(data)).toEqual({
  76. totalInvocationCount: 23,
  77. totalWarningCount: 4,
  78. totalErrorCount: 5,
  79. })
  80. })
  81. it('builds segmented button classes and chart range labels', () => {
  82. expect(getSegmentedButtonClassName(0, 4)).toBe('rounded-tr-none rounded-br-none')
  83. expect(getSegmentedButtonClassName(1, 4)).toBe('rounded-none')
  84. expect(getSegmentedButtonClassName(3, 4)).toBe('rounded-tl-none rounded-bl-none')
  85. expect(
  86. getChartTimeRangeLabels(
  87. [{ timestamp: '2026-03-20T10:00:00.000Z' }, { timestamp: '2026-03-20T11:00:00.000Z' }],
  88. 'MMM D, h:mma'
  89. )
  90. ).toEqual({
  91. start: dayjs('2026-03-20T10:00:00.000Z').format('MMM D, h:mma'),
  92. end: dayjs('2026-03-20T11:00:00.000Z').format('MMM D, h:mma'),
  93. })
  94. expect(getChartTimeRangeLabels([], 'MMM D')).toBeUndefined()
  95. expect(formatChartTimestamp('2026-03-20T10:00:00.000Z', 'MMM D, h:mma')).toBe(
  96. dayjs('2026-03-20T10:00:00.000Z').format('MMM D, h:mma')
  97. )
  98. })
  99. it('computes execution and usage metrics', () => {
  100. const stats = [
  101. {
  102. timestamp: '2026-03-20T10:00:00.000Z',
  103. success_count: 0,
  104. redirect_count: 0,
  105. client_err_count: 0,
  106. server_err_count: 0,
  107. avg_execution_time: 10,
  108. max_execution_time: 18,
  109. avg_cpu_time_used: 5,
  110. max_cpu_time_used: 8,
  111. avg_memory_used: 100,
  112. avg_heap_memory_used: 75,
  113. avg_external_memory_used: 25,
  114. },
  115. {
  116. timestamp: '2026-03-20T10:15:00.000Z',
  117. success_count: 0,
  118. redirect_count: 0,
  119. client_err_count: 0,
  120. server_err_count: 0,
  121. avg_execution_time: 30,
  122. max_execution_time: 45,
  123. avg_cpu_time_used: 15,
  124. max_cpu_time_used: 20,
  125. avg_memory_used: 200,
  126. avg_heap_memory_used: 150,
  127. avg_external_memory_used: 50,
  128. },
  129. ]
  130. expect(getExecutionMetrics(stats)).toEqual({
  131. averageExecutionTime: 20,
  132. maxExecutionTime: 45,
  133. })
  134. expect(getUsageMetrics(stats)).toEqual({
  135. averageCpuTime: 10,
  136. maxCpuTime: 20,
  137. averageMemoryUsage: 150,
  138. totalHeapMemory: 225,
  139. totalExternalMemory: 75,
  140. totalMemoryByType: 300,
  141. })
  142. })
  143. it('returns a snapped deploy annotation when updated_at falls within the selected window', () => {
  144. const annotation = getInvocationUpdateAnnotation({
  145. updatedAt: '2026-03-20T10:16:30.000Z',
  146. invocationChartData: [
  147. { timestamp: '2026-03-20T10:00:00.000Z', ok_count: 3, warning_count: 0, error_count: 0 },
  148. { timestamp: '2026-03-20T10:15:00.000Z', ok_count: 5, warning_count: 1, error_count: 1 },
  149. { timestamp: '2026-03-20T10:30:00.000Z', ok_count: 2, warning_count: 0, error_count: 0 },
  150. ],
  151. windowStart: new Date('2026-03-20T09:45:00.000Z'),
  152. windowEnd: new Date('2026-03-20T10:45:00.000Z'),
  153. })
  154. expect(annotation?.timestamp).toBe('2026-03-20T10:15:00.000Z')
  155. expect(annotation?.position).toBeCloseTo(50)
  156. expect(annotation?.updatedAt.toISOString()).toBe('2026-03-20T10:16:30.000Z')
  157. })
  158. it('hides the deploy annotation when updated_at is outside the selected window', () => {
  159. const annotation = getInvocationUpdateAnnotation({
  160. updatedAt: '2026-03-20T11:05:00.000Z',
  161. invocationChartData: [
  162. { timestamp: '2026-03-20T10:00:00.000Z', ok_count: 3, warning_count: 0, error_count: 0 },
  163. ],
  164. windowStart: new Date('2026-03-20T09:45:00.000Z'),
  165. windowEnd: new Date('2026-03-20T10:45:00.000Z'),
  166. })
  167. expect(annotation).toBeUndefined()
  168. })
  169. it('builds bucketed and rolling time windows from the selected interval', () => {
  170. const interval = EDGE_FUNCTION_CHART_INTERVALS.find((item) => item.key === '1hr')
  171. expect(interval).toBeDefined()
  172. const now = new Date('2026-03-20T10:37:00.000Z')
  173. const [bucketedStart, bucketedEnd] = getBucketedTimeRange(interval!, now)
  174. const [rollingStart, rollingEnd] = getRollingTimeRange(interval!, now)
  175. expect(bucketedStart.toISOString()).toBe('2026-03-20T09:00:00.000Z')
  176. expect(bucketedEnd.toISOString()).toBe('2026-03-20T10:00:00.000Z')
  177. expect(rollingStart.toISOString()).toBe('2026-03-20T09:37:00.000Z')
  178. expect(rollingEnd.toISOString()).toBe('2026-03-20T10:37:00.000Z')
  179. })
  180. it('formats metric, rate, and reference deltas consistently', () => {
  181. expect(formatMetric(12.34, 'MB')).toBe('12.3MB')
  182. expect(formatMetric(1234, 'ms')).toBe('1,234ms')
  183. expect(formatRate(1, 4)).toBe('25%')
  184. expect(formatReferenceDelta(110, 100)).toBe('10% above average')
  185. expect(formatReferenceDelta(90, 100)).toBe('10% below average')
  186. expect(formatReferenceDelta(100, 100)).toBe('At average')
  187. })
  188. it('builds empty-state copy and tooltip detail strings', () => {
  189. expect(getChartEmptyStateCopy('invocations', false, 'boom')).toEqual({
  190. title: 'No data to show',
  191. description: undefined,
  192. })
  193. expect(getChartEmptyStateCopy('CPU time', true, 'Request failed')).toEqual({
  194. title: 'Unable to load CPU time',
  195. description: 'Request failed',
  196. })
  197. expect(getMemoryTooltipDetail(12.34, 5.67)).toBe('Heap 12.3MB • External 5.7MB')
  198. })
  199. })