StatsManager.test.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { expect, describe, it, vi } from 'vitest'
  2. import StatsManager from '../../../app/js/StatsManager.js'
  3. const { sampleByHash, sampleRequest } = StatsManager
  4. // Mocks allow us to import Metrics.js twice without getting errors.
  5. vi.mock('@overleaf/metrics', () => ({
  6. prom: {
  7. Gauge: vi.fn(),
  8. Counter: vi.fn(),
  9. Histogram: vi.fn(),
  10. },
  11. }))
  12. describe('StatsManager', function () {
  13. describe('sampleByHash', function () {
  14. it('should always return false for a sample percentage of 0', function () {
  15. for (let i = 0; i < 100; i++) {
  16. const key = `test-key-${i}`
  17. expect(sampleByHash(key, 0), `key ${key} should be false`).to.be.false
  18. }
  19. })
  20. it('should always return false for a negative sample percentage', function () {
  21. for (let i = 0; i < 100; i++) {
  22. const key = `test-key-${i}`
  23. expect(sampleByHash(key, -10), `key ${key} should be false`).to.be.false
  24. }
  25. })
  26. it('should always return true for a sample percentage of 100', function () {
  27. // This isn't strictly true, if the hash is exactly 0xffffffff, then the percentile is 100
  28. // and 100 < 100 is false. But the chances of that are 1 in 4 billion.
  29. for (let i = 0; i < 100; i++) {
  30. const key = `test-key-${i}`
  31. expect(sampleByHash(key, 100), `key ${key} should be true`).to.be.true
  32. }
  33. })
  34. it('should return the expected number of results for a sample percentage of 75', function () {
  35. // This isn't strictly true, if the hash is exactly 0xffffffff, then the percentile is 100
  36. // and 100 < 100 is false. But the chances of that are 1 in 4 billion.
  37. let count = 0
  38. for (let i = 0; i < 100; i++) {
  39. const key = `test-key-${i}`
  40. count += sampleByHash(key, 75) ? 1 : 0
  41. }
  42. // Actual result is 74, it's deterministic but the test allows the algorithm to change
  43. expect(count).to.be.within(70, 80)
  44. })
  45. it('should return true when the hash is within the sample percentage', function () {
  46. // The MD5 hash of 'test-key-in' gives a percentile of 13
  47. const key = 'test-key-in'
  48. const percentage = 40
  49. expect(sampleByHash(key, percentage)).to.be.true
  50. })
  51. it('should return false when the hash is outside the sample percentage', function () {
  52. // The MD5 hash of 'test-key-outer' gives a percentile of 47
  53. const key = 'test-key-outer'
  54. const percentage = 40
  55. expect(sampleByHash(key, percentage)).to.be.false
  56. })
  57. it('should produce consistent results for the same key', function () {
  58. const key = 'consistent-key'
  59. const percentage = 50
  60. const result1 = sampleByHash(key, percentage)
  61. const result2 = sampleByHash(key, percentage)
  62. expect(result1).to.equal(result2)
  63. })
  64. it('should handle different keys correctly', function () {
  65. // MD5('key1') => percentile 76
  66. // MD5('key2') => percentile 47
  67. expect(sampleByHash('key1', 80)).to.be.true
  68. expect(sampleByHash('key1', 70)).to.be.false
  69. expect(sampleByHash('key2', 50)).to.be.true
  70. expect(sampleByHash('key2', 40)).to.be.false
  71. })
  72. it('should be monotonic with respect to percentage', function () {
  73. const key = 'test-key'
  74. const percentile = 32
  75. for (let i = 0; i <= 100; i++) {
  76. const result = sampleByHash(key, i)
  77. if (i <= percentile) {
  78. expect(result, `percentage ${i} should be false`).to.be.false
  79. } else {
  80. expect(result, `percentage ${i} should be true`).to.be.true
  81. }
  82. }
  83. })
  84. })
  85. describe('sampleRequest', function () {
  86. it('should return undefined if there is no user_id', function () {
  87. const request = { metricsOpts: {} }
  88. const percentage = 50
  89. expect(sampleRequest(request, percentage)).to.be.undefined
  90. })
  91. it('should return undefined if the path is health-check', function () {
  92. const request = {
  93. user_id: 'some-user',
  94. metricsOpts: { path: 'health-check' },
  95. }
  96. const percentage = 100
  97. expect(sampleRequest(request, percentage)).to.be.undefined
  98. })
  99. it('should return undefined if the path is clsi-perf', function () {
  100. const request = {
  101. user_id: 'some-user',
  102. metricsOpts: { path: 'clsi-perf' },
  103. }
  104. const percentage = 100
  105. expect(sampleRequest(request, percentage)).to.be.undefined
  106. })
  107. it('should return undefined for a health-check even if the user would be sampled', function () {
  108. const request = {
  109. user_id: 'test-key-in', // percentile 13
  110. metricsOpts: { path: 'health-check' },
  111. }
  112. const percentage = 40
  113. expect(sampleRequest(request, percentage)).to.be.undefined
  114. })
  115. it('should return undefined for clsi-perf even if the user would be sampled', function () {
  116. const request = {
  117. user_id: 'test-key-in', // percentile 13
  118. metricsOpts: { path: 'clsi-perf' },
  119. }
  120. const percentage = 40
  121. expect(sampleRequest(request, percentage)).to.be.undefined
  122. })
  123. it('should return undefined if the sampling percentage is 0', function () {
  124. const request = { user_id: 'some-user', metricsOpts: {} }
  125. const percentage = 0
  126. expect(sampleRequest(request, percentage)).to.be.undefined
  127. })
  128. it('should return undefined if the sampling percentage is negative', function () {
  129. const request = { user_id: 'some-user', metricsOpts: {} }
  130. const percentage = -10
  131. expect(sampleRequest(request, percentage)).to.be.undefined
  132. })
  133. it('should sample if metricsOpts has no path', function () {
  134. const request = { user_id: 'test-key-in', metricsOpts: {} } // percentile 13
  135. const percentage = 40
  136. expect(sampleRequest(request, percentage)).to.be.true
  137. })
  138. it('should return true for a request that should be sampled', function () {
  139. const request = { user_id: 'test-key-in', metricsOpts: {} } // percentile 13
  140. const percentage = 40
  141. expect(sampleRequest(request, percentage)).to.be.true
  142. })
  143. it('should return false for a request that should not be sampled', function () {
  144. const request = { user_id: 'test-key-outer', metricsOpts: {} } // percentile 47
  145. const percentage = 40
  146. expect(sampleRequest(request, percentage)).to.be.false
  147. })
  148. })
  149. })