analyzeFrames.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import {render, screen} from 'sentry-test/reactTestingLibrary';
  2. import {textWithMarkupMatcher} from 'sentry-test/utils';
  3. import {
  4. analyzeFrameForRootCause,
  5. analyzeFramesForRootCause,
  6. } from 'sentry/components/events/interfaces/analyzeFrames';
  7. import type {Event, Frame} from 'sentry/types/event';
  8. import {EntryType, EventOrGroupType, LockType} from 'sentry/types/event';
  9. const makeEventWithFrames = (frames: Frame[]): Event => {
  10. const event: Event = {
  11. id: '020eb33f6ce64ed6adc60f8993535816',
  12. groupID: '68',
  13. eventID: '020eb33f6ce64ed6adc60f8993535816',
  14. projectID: '2',
  15. size: 3481,
  16. entries: [
  17. {
  18. data: {
  19. values: [
  20. {
  21. type: 'ZeroDivisionError',
  22. value: 'divided by 0',
  23. mechanism: null,
  24. threadId: null,
  25. module: '',
  26. stacktrace: {
  27. frames: [
  28. {
  29. filename: 'puma (3.12.6) lib/puma/thread_pool.rb',
  30. absPath: 'puma (3.12.6) lib/puma/thread_pool.rb',
  31. module: null,
  32. package: null,
  33. platform: null,
  34. instructionAddr: null,
  35. symbolAddr: null,
  36. function: 'block in spawn_thread',
  37. rawFunction: null,
  38. symbol: null,
  39. context: [],
  40. lineNo: 135,
  41. colNo: null,
  42. inApp: false,
  43. trust: null,
  44. vars: null,
  45. },
  46. ...frames,
  47. ],
  48. framesOmitted: null,
  49. registers: null,
  50. hasSystemFrames: true,
  51. },
  52. rawStacktrace: null,
  53. frames: null,
  54. },
  55. ],
  56. hasSystemFrames: true,
  57. excOmitted: null,
  58. },
  59. type: EntryType.EXCEPTION,
  60. },
  61. {
  62. data: {
  63. values: [
  64. {
  65. id: 13920,
  66. current: true,
  67. crashed: true,
  68. name: 'puma 002',
  69. stacktrace: null,
  70. rawStacktrace: null,
  71. state: 'WAITING',
  72. },
  73. ],
  74. },
  75. type: EntryType.THREADS,
  76. },
  77. ],
  78. dist: null,
  79. message: '',
  80. title: 'ZeroDivisionError: divided by 0',
  81. location: 'sentry/controllers/welcome_controller.rb',
  82. user: null,
  83. contexts: {},
  84. sdk: null,
  85. context: {},
  86. packages: {},
  87. type: EventOrGroupType.ERROR,
  88. metadata: {
  89. display_title_with_tree_label: false,
  90. filename: 'sentry/controllers/welcome_controller.rb',
  91. finest_tree_label: [
  92. {filebase: 'welcome_controller.rb', function: '/'},
  93. {filebase: 'welcome_controller.rb', function: 'index'},
  94. ],
  95. function: '/',
  96. type: 'ZeroDivisionError',
  97. value: 'divided by 0',
  98. },
  99. tags: [{key: 'level', value: 'error'}],
  100. platform: 'other',
  101. dateReceived: '2021-10-28T12:28:22.318469Z',
  102. errors: [],
  103. crashFile: null,
  104. culprit: 'sentry/controllers/welcome_controller.rb in /',
  105. dateCreated: '2021-10-28T12:28:22.318469Z',
  106. fingerprints: ['58f1f47bea5239ea25397888dc9253d1'],
  107. groupingConfig: {
  108. enhancements: 'eJybzDRxY25-UmZOqpWRgZGhroGJroHRBABbUQb_',
  109. id: 'mobile:2021-02-12',
  110. },
  111. release: null,
  112. userReport: null,
  113. sdkUpdates: [],
  114. nextEventID: null,
  115. previousEventID: null,
  116. occurrence: null,
  117. };
  118. return event;
  119. };
  120. describe('analyzeAnrFrames', function () {
  121. it('detects anr root cause', function () {
  122. const event = makeEventWithFrames([
  123. {
  124. filename: 'SharedPreferencesImpl.java',
  125. absPath: 'SharedPreferencesImpl.java',
  126. module: 'android.app.SharedPreferencesImpl$EditorImpl$1',
  127. package: null,
  128. platform: null,
  129. instructionAddr: null,
  130. symbolAddr: null,
  131. function: 'run',
  132. rawFunction: null,
  133. symbol: null,
  134. context: [],
  135. lineNo: 366,
  136. colNo: null,
  137. inApp: false,
  138. trust: null,
  139. vars: null,
  140. },
  141. ]);
  142. const rootCause = analyzeFramesForRootCause(event);
  143. expect(rootCause?.resources).toEqual(
  144. 'SharedPreferences.apply will save data on background thread only if it happens before the activity/service finishes. Switch to SharedPreferences.commit and move commit to a background thread.'
  145. );
  146. expect(rootCause?.culprit).toEqual(
  147. '/^android\\.app\\.SharedPreferencesImpl\\$EditorImpl\\$[0-9]/'
  148. );
  149. });
  150. it('regex matches function names', function () {
  151. const event = makeEventWithFrames([
  152. {
  153. filename: 'sqlite.SQLiteConnection',
  154. absPath: 'sqlite.SQLiteConnection',
  155. module: 'android.database.sqlite.SQLiteConnection',
  156. package: null,
  157. platform: null,
  158. instructionAddr: null,
  159. symbolAddr: null,
  160. function: 'nativeBindArgs',
  161. rawFunction: null,
  162. symbol: null,
  163. context: [],
  164. lineNo: 366,
  165. colNo: null,
  166. inApp: false,
  167. trust: null,
  168. vars: null,
  169. },
  170. ]);
  171. const rootCause = analyzeFramesForRootCause(event);
  172. expect(rootCause?.resources).toEqual(
  173. 'Database operations, such as querying, inserting, updating, or deleting data, can involve disk I/O, processing, and potentially long-running operations. Move database operations off the main thread to avoid this ANR.'
  174. );
  175. expect(rootCause?.culprit).toEqual('android.database.sqlite.SQLiteConnection');
  176. });
  177. it('picks anr root cause of the topmost frame', function () {
  178. const event = makeEventWithFrames([
  179. {
  180. filename: 'Instrumentation.java',
  181. absPath: 'Instrumentation.java',
  182. module: 'android.app.Instrumentation',
  183. package: null,
  184. platform: null,
  185. instructionAddr: null,
  186. symbolAddr: null,
  187. function: 'callApplicationOnCreate',
  188. rawFunction: null,
  189. symbol: null,
  190. context: [],
  191. lineNo: 1176,
  192. colNo: null,
  193. inApp: false,
  194. trust: null,
  195. vars: null,
  196. },
  197. {
  198. filename: 'SharedPreferencesImpl.java',
  199. absPath: 'SharedPreferencesImpl.java',
  200. module: 'android.app.SharedPreferencesImpl$EditorImpl$1',
  201. package: null,
  202. platform: null,
  203. instructionAddr: null,
  204. symbolAddr: null,
  205. function: 'run',
  206. rawFunction: null,
  207. symbol: null,
  208. context: [],
  209. lineNo: 366,
  210. colNo: null,
  211. inApp: false,
  212. trust: null,
  213. vars: null,
  214. },
  215. ]);
  216. const rootCause = analyzeFramesForRootCause(event);
  217. expect(rootCause?.resources).toEqual(
  218. 'SharedPreferences.apply will save data on background thread only if it happens before the activity/service finishes. Switch to SharedPreferences.commit and move commit to a background thread.'
  219. );
  220. expect(rootCause?.culprit).toEqual(
  221. '/^android\\.app\\.SharedPreferencesImpl\\$EditorImpl\\$[0-9]/'
  222. );
  223. });
  224. it('given lock address returns frame with matching lock address', function () {
  225. const frame1 = {
  226. filename: 'Instrumentation.java',
  227. absPath: 'Instrumentation.java',
  228. module: 'android.app.Instrumentation',
  229. package: null,
  230. platform: null,
  231. instructionAddr: null,
  232. symbolAddr: null,
  233. function: 'call',
  234. rawFunction: null,
  235. symbol: null,
  236. context: [],
  237. lineNo: 1176,
  238. colNo: null,
  239. inApp: false,
  240. trust: null,
  241. vars: null,
  242. lock: {
  243. type: LockType.BLOCKED,
  244. address: '0x08a8651c',
  245. package_name: 'io.sentry.samples',
  246. class_name: 'Monitor',
  247. thread_id: 12,
  248. },
  249. };
  250. const frame2 = {
  251. filename: 'MainActivity.java',
  252. absPath: 'MainActivity.java',
  253. module: 'com.example.MainActivity',
  254. package: null,
  255. platform: null,
  256. instructionAddr: null,
  257. symbolAddr: null,
  258. function: 'onCreate',
  259. rawFunction: null,
  260. symbol: null,
  261. context: [],
  262. lineNo: 128,
  263. colNo: null,
  264. inApp: false,
  265. trust: null,
  266. vars: null,
  267. lock: {
  268. type: LockType.BLOCKED,
  269. address: '0x07d7437b',
  270. package_name: 'java.lang',
  271. class_name: 'Object',
  272. thread_id: 7,
  273. },
  274. };
  275. const rootCause1 = analyzeFrameForRootCause(frame1, undefined, '<address>');
  276. expect(rootCause1).toBeNull();
  277. const rootCause2 = analyzeFrameForRootCause(frame2, undefined, '0x07d7437b');
  278. render(<div>{rootCause2?.resources}</div>);
  279. expect(
  280. screen.getByText(
  281. textWithMarkupMatcher(
  282. 'The main thread is blocked/waiting, trying to acquire lock 0x07d7437b (java.lang.Object) held by the suspect frame of this thread.'
  283. )
  284. )
  285. ).toBeInTheDocument();
  286. });
  287. it('when thread id is not provided, does not append "held by"', function () {
  288. const frame = {
  289. filename: 'MainActivity.java',
  290. absPath: 'MainActivity.java',
  291. module: 'com.example.MainActivity',
  292. package: null,
  293. platform: null,
  294. instructionAddr: null,
  295. symbolAddr: null,
  296. function: 'onCreate',
  297. rawFunction: null,
  298. symbol: null,
  299. context: [],
  300. lineNo: 128,
  301. colNo: null,
  302. inApp: false,
  303. trust: null,
  304. vars: null,
  305. lock: {
  306. type: LockType.BLOCKED,
  307. address: '0x07d7437b',
  308. package_name: 'java.lang',
  309. class_name: 'Object',
  310. },
  311. };
  312. const rootCause2 = analyzeFrameForRootCause(frame, undefined, '0x07d7437b');
  313. render(<div>{rootCause2?.resources}</div>);
  314. expect(
  315. screen.getByText(
  316. textWithMarkupMatcher(
  317. 'The main thread is blocked/waiting, trying to acquire lock 0x07d7437b (java.lang.Object) .'
  318. )
  319. )
  320. ).toBeInTheDocument();
  321. });
  322. it('given main thread is locked returns it as root cause', function () {
  323. const frame = {
  324. filename: 'MainActivity.java',
  325. absPath: 'MainActivity.java',
  326. module: 'com.example.MainActivity',
  327. package: null,
  328. platform: null,
  329. instructionAddr: null,
  330. symbolAddr: null,
  331. function: 'onCreate',
  332. rawFunction: null,
  333. symbol: null,
  334. context: [],
  335. lineNo: 128,
  336. colNo: null,
  337. inApp: false,
  338. trust: null,
  339. vars: null,
  340. lock: {
  341. type: LockType.BLOCKED,
  342. address: '0x08a1321b',
  343. package_name: 'java.lang',
  344. class_name: 'Object',
  345. thread_id: 7,
  346. },
  347. };
  348. const thread = {
  349. id: 13920,
  350. current: true,
  351. crashed: true,
  352. name: 'puma 002',
  353. stacktrace: null,
  354. rawStacktrace: null,
  355. state: 'BLOCKED',
  356. };
  357. const rootCause = analyzeFrameForRootCause(frame, thread);
  358. render(<div>{rootCause?.resources}</div>);
  359. expect(
  360. screen.getByText(
  361. textWithMarkupMatcher(
  362. 'The main thread is blocked/waiting, trying to acquire lock 0x08a1321b (java.lang.Object) held by the suspect frame of this thread.'
  363. )
  364. )
  365. ).toBeInTheDocument();
  366. });
  367. });