instrumentation-client.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import type { Event as SentryEvent, StackFrame } from '@sentry/nextjs'
  2. import { describe, expect, it } from 'vitest'
  3. import {
  4. isBrowserWalletExtensionError,
  5. isCancellationRejection,
  6. isChallengeExpiredError,
  7. isUserAbortedOperation,
  8. } from './instrumentation-client'
  9. describe('Sentry beforeSend filtering functions', () => {
  10. describe('isBrowserWalletExtensionError', () => {
  11. it('returns true for Gate.io wallet extension error (gt-window-provider.js)', () => {
  12. const event: SentryEvent = {
  13. exception: {
  14. values: [
  15. {
  16. type: 'TypeError',
  17. value: 'en.shouldSetTallyForCurrentProvider is not a function',
  18. stacktrace: {
  19. frames: [
  20. { filename: 'app:///_next/static/chunks/main.js' } as StackFrame,
  21. { filename: 'app:///gt-window-provider.js' } as StackFrame,
  22. ],
  23. },
  24. },
  25. ],
  26. },
  27. }
  28. expect(isBrowserWalletExtensionError(event)).toBe(true)
  29. })
  30. it('returns true for Gate.io BTC wallet extension error (gt-window-provider-btc.js)', () => {
  31. const event: SentryEvent = {
  32. exception: {
  33. values: [
  34. {
  35. type: 'TypeError',
  36. value: 'f.shouldSetTallyForCurrentProvider is not a function',
  37. stacktrace: {
  38. frames: [{ filename: 'app:///gt-window-provider-btc.js' } as StackFrame],
  39. },
  40. },
  41. ],
  42. },
  43. }
  44. expect(isBrowserWalletExtensionError(event)).toBe(true)
  45. })
  46. it('returns true for wallet-provider in abs_path', () => {
  47. const event: SentryEvent = {
  48. exception: {
  49. values: [
  50. {
  51. type: 'Error',
  52. value: 'wallet error',
  53. stacktrace: {
  54. frames: [
  55. { abs_path: 'chrome-extension://abc123/wallet-provider.js' } as StackFrame,
  56. ],
  57. },
  58. },
  59. ],
  60. },
  61. }
  62. expect(isBrowserWalletExtensionError(event)).toBe(true)
  63. })
  64. it('returns false for regular application errors', () => {
  65. const event: SentryEvent = {
  66. exception: {
  67. values: [
  68. {
  69. type: 'Error',
  70. value: 'Regular error',
  71. stacktrace: {
  72. frames: [
  73. { filename: 'app:///_next/static/chunks/main.js' } as StackFrame,
  74. { filename: 'app:///_next/static/chunks/pages/index.js' } as StackFrame,
  75. ],
  76. },
  77. },
  78. ],
  79. },
  80. }
  81. expect(isBrowserWalletExtensionError(event)).toBe(false)
  82. })
  83. it('returns false for empty event', () => {
  84. const event: SentryEvent = {}
  85. expect(isBrowserWalletExtensionError(event)).toBe(false)
  86. })
  87. it('returns false when exception values are empty', () => {
  88. const event: SentryEvent = {
  89. exception: {
  90. values: [],
  91. },
  92. }
  93. expect(isBrowserWalletExtensionError(event)).toBe(false)
  94. })
  95. it('returns false when stacktrace frames are undefined', () => {
  96. const event: SentryEvent = {
  97. exception: {
  98. values: [
  99. {
  100. type: 'Error',
  101. value: 'Error without stacktrace',
  102. },
  103. ],
  104. },
  105. }
  106. expect(isBrowserWalletExtensionError(event)).toBe(false)
  107. })
  108. })
  109. describe('isUserAbortedOperation', () => {
  110. it('returns true for "operation was aborted" error', () => {
  111. const error = new Error('The operation was aborted.')
  112. const event: SentryEvent = {}
  113. expect(isUserAbortedOperation(error, event)).toBe(true)
  114. })
  115. it('returns true for "signal is aborted" error', () => {
  116. const error = new Error('signal is aborted without reason')
  117. const event: SentryEvent = {}
  118. expect(isUserAbortedOperation(error, event)).toBe(true)
  119. })
  120. it('returns true for "manually canceled" error', () => {
  121. const error = new Error('operation is manually canceled')
  122. const event: SentryEvent = {}
  123. expect(isUserAbortedOperation(error, event)).toBe(true)
  124. })
  125. it('returns true for "AbortError" message', () => {
  126. const error = new Error('AbortError: The operation was aborted')
  127. const event: SentryEvent = {}
  128. expect(isUserAbortedOperation(error, event)).toBe(true)
  129. })
  130. it('returns true when message is in event.message (no error object)', () => {
  131. const error = null
  132. const event: SentryEvent = {
  133. message: '[CRITICAL][sign in via EP] Failed: The operation was aborted.',
  134. }
  135. expect(isUserAbortedOperation(error, event)).toBe(true)
  136. })
  137. it('returns true for event message with "signal is aborted"', () => {
  138. const event: SentryEvent = {
  139. message: '[CRITICAL][sign in via EP] Failed: signal is aborted without reason',
  140. }
  141. expect(isUserAbortedOperation(undefined, event)).toBe(true)
  142. })
  143. it('returns false for regular errors', () => {
  144. const error = new Error('Something went wrong')
  145. const event: SentryEvent = {}
  146. expect(isUserAbortedOperation(error, event)).toBe(false)
  147. })
  148. it('returns false for empty inputs', () => {
  149. expect(isUserAbortedOperation(null, {})).toBe(false)
  150. expect(isUserAbortedOperation(undefined, {})).toBe(false)
  151. })
  152. it('handles non-Error objects gracefully', () => {
  153. const error = { message: 'The operation was aborted.' }
  154. const event: SentryEvent = {}
  155. // Non-Error objects should not match since we check instanceof Error
  156. expect(isUserAbortedOperation(error, event)).toBe(false)
  157. })
  158. })
  159. describe('isCancellationRejection', () => {
  160. it('returns true for cancellation type in extra.__serialized__', () => {
  161. const event: SentryEvent = {
  162. extra: {
  163. __serialized__: {
  164. msg: 'operation is manually canceled',
  165. type: 'cancelation',
  166. },
  167. },
  168. }
  169. expect(isCancellationRejection(event)).toBe(true)
  170. })
  171. it('returns false when type is not cancelation', () => {
  172. const event: SentryEvent = {
  173. extra: {
  174. __serialized__: {
  175. msg: 'some error',
  176. type: 'error',
  177. },
  178. },
  179. }
  180. expect(isCancellationRejection(event)).toBe(false)
  181. })
  182. it('returns false when __serialized__ is undefined', () => {
  183. const event: SentryEvent = {
  184. extra: {},
  185. }
  186. expect(isCancellationRejection(event)).toBe(false)
  187. })
  188. it('returns false when extra is undefined', () => {
  189. const event: SentryEvent = {}
  190. expect(isCancellationRejection(event)).toBe(false)
  191. })
  192. it('returns false when __serialized__ has no type property', () => {
  193. const event: SentryEvent = {
  194. extra: {
  195. __serialized__: {
  196. msg: 'some message',
  197. },
  198. },
  199. }
  200. expect(isCancellationRejection(event)).toBe(false)
  201. })
  202. })
  203. describe('isChallengeExpiredError', () => {
  204. it('returns true for challenge-expired error message', () => {
  205. const error = new Error('Non-Error promise rejection captured with value: challenge-expired')
  206. const event: SentryEvent = {}
  207. expect(isChallengeExpiredError(error, event)).toBe(true)
  208. })
  209. it('returns true when challenge-expired is in event.message', () => {
  210. const event: SentryEvent = {
  211. message: 'challenge-expired',
  212. }
  213. expect(isChallengeExpiredError(null, event)).toBe(true)
  214. })
  215. it('returns false for regular errors', () => {
  216. const error = new Error('Something went wrong')
  217. const event: SentryEvent = {}
  218. expect(isChallengeExpiredError(error, event)).toBe(false)
  219. })
  220. it('returns false for empty inputs', () => {
  221. expect(isChallengeExpiredError(null, {})).toBe(false)
  222. expect(isChallengeExpiredError(undefined, {})).toBe(false)
  223. })
  224. it('returns false for similar but different messages', () => {
  225. const error = new Error('challenge expired') // No hyphen
  226. const event: SentryEvent = {}
  227. expect(isChallengeExpiredError(error, event)).toBe(false)
  228. })
  229. })
  230. describe('integration scenarios', () => {
  231. it('correctly identifies BRIVEN-APP-353 pattern (cancellation rejection)', () => {
  232. // Based on actual Sentry issue BRIVEN-APP-353
  233. const event: SentryEvent = {
  234. exception: {
  235. values: [
  236. {
  237. type: 'UnhandledRejection',
  238. value: 'Object captured as promise rejection with keys: msg, type',
  239. },
  240. ],
  241. },
  242. extra: {
  243. __serialized__: {
  244. msg: 'operation is manually canceled',
  245. type: 'cancelation',
  246. },
  247. },
  248. }
  249. expect(isCancellationRejection(event)).toBe(true)
  250. })
  251. it('correctly identifies BRIVEN-APP-AFC pattern (wallet extension)', () => {
  252. // Based on actual Sentry issue BRIVEN-APP-AFC
  253. const event: SentryEvent = {
  254. exception: {
  255. values: [
  256. {
  257. type: 'TypeError',
  258. value: 'f.shouldSetTallyForCurrentProvider is not a function',
  259. stacktrace: {
  260. frames: [
  261. {
  262. filename:
  263. 'node_modules/.pnpm/@sentry+browser@10.27.0/node_modules/@sentry/browser/src/helpers.ts',
  264. function: 'n',
  265. } as StackFrame,
  266. {
  267. filename: 'app:///gt-window-provider-btc.js',
  268. function: 'GateWindowProvider.internalListener',
  269. } as StackFrame,
  270. ],
  271. },
  272. },
  273. ],
  274. },
  275. }
  276. expect(isBrowserWalletExtensionError(event)).toBe(true)
  277. })
  278. it('correctly identifies BRIVEN-APP-92A pattern (wallet extension)', () => {
  279. // Based on actual Sentry issue BRIVEN-APP-92A
  280. const event: SentryEvent = {
  281. exception: {
  282. values: [
  283. {
  284. type: 'TypeError',
  285. value: 'en.shouldSetTallyForCurrentProvider is not a function',
  286. stacktrace: {
  287. frames: [
  288. {
  289. filename:
  290. 'node_modules/.pnpm/@sentry+browser@10.27.0/node_modules/@sentry/browser/src/helpers.ts',
  291. function: 'n',
  292. } as StackFrame,
  293. {
  294. filename: 'app:///gt-window-provider.js',
  295. function: 'GateWindowProvider.internalListener',
  296. } as StackFrame,
  297. ],
  298. },
  299. },
  300. ],
  301. },
  302. }
  303. expect(isBrowserWalletExtensionError(event)).toBe(true)
  304. })
  305. it('correctly identifies BRIVEN-APP-BG6 pattern (user aborted)', () => {
  306. // Based on actual Sentry issue BRIVEN-APP-BG6
  307. const error = new Error('The operation was aborted.')
  308. const event: SentryEvent = {
  309. message: '[CRITICAL][sign in via EP] Failed: The operation was aborted.',
  310. }
  311. expect(isUserAbortedOperation(error, event)).toBe(true)
  312. })
  313. it('correctly identifies BRIVEN-APP-BG7 pattern (signal aborted)', () => {
  314. // Based on actual Sentry issue BRIVEN-APP-BG7
  315. const error = new Error('signal is aborted without reason')
  316. const event: SentryEvent = {
  317. message: '[CRITICAL][sign in via EP] Failed: signal is aborted without reason',
  318. }
  319. expect(isUserAbortedOperation(error, event)).toBe(true)
  320. })
  321. it('correctly identifies BRIVEN-APP-ACC pattern (challenge expired)', () => {
  322. // Based on actual Sentry issue BRIVEN-APP-ACC
  323. const error = new Error('Non-Error promise rejection captured with value: challenge-expired')
  324. const event: SentryEvent = {
  325. exception: {
  326. values: [
  327. {
  328. type: 'UnhandledRejection',
  329. value: 'Non-Error promise rejection captured with value: challenge-expired',
  330. },
  331. ],
  332. },
  333. }
  334. expect(isChallengeExpiredError(error, event)).toBe(true)
  335. })
  336. it('does not filter legitimate errors', () => {
  337. const error = new Error('Cannot read property "foo" of undefined')
  338. const event: SentryEvent = {
  339. exception: {
  340. values: [
  341. {
  342. type: 'TypeError',
  343. value: 'Cannot read property "foo" of undefined',
  344. stacktrace: {
  345. frames: [{ filename: 'app:///_next/static/chunks/pages/index.js' } as StackFrame],
  346. },
  347. },
  348. ],
  349. },
  350. }
  351. expect(isBrowserWalletExtensionError(event)).toBe(false)
  352. expect(isUserAbortedOperation(error, event)).toBe(false)
  353. expect(isCancellationRejection(event)).toBe(false)
  354. expect(isChallengeExpiredError(error, event)).toBe(false)
  355. })
  356. })
  357. })