analyzeFrames.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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 {EntryType, Event, EventOrGroupType, Frame, LockType} from 'sentry/types/event';
  8. const makeEventWithFrames = (frames: Frame[]): Event => {
  9. const event: Event = {
  10. id: '020eb33f6ce64ed6adc60f8993535816',
  11. groupID: '68',
  12. eventID: '020eb33f6ce64ed6adc60f8993535816',
  13. projectID: '2',
  14. size: 3481,
  15. entries: [
  16. {
  17. data: {
  18. values: [
  19. {
  20. type: 'ZeroDivisionError',
  21. value: 'divided by 0',
  22. mechanism: null,
  23. threadId: null,
  24. module: '',
  25. stacktrace: {
  26. frames: [
  27. {
  28. filename: 'puma (3.12.6) lib/puma/thread_pool.rb',
  29. absPath: 'puma (3.12.6) lib/puma/thread_pool.rb',
  30. module: null,
  31. package: null,
  32. platform: null,
  33. instructionAddr: null,
  34. symbolAddr: null,
  35. function: 'block in spawn_thread',
  36. rawFunction: null,
  37. symbol: null,
  38. context: [],
  39. lineNo: 135,
  40. colNo: null,
  41. inApp: false,
  42. trust: null,
  43. errors: 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. errors: null,
  140. vars: null,
  141. },
  142. ]);
  143. const rootCause = analyzeFramesForRootCause(event);
  144. expect(rootCause?.resources).toEqual(
  145. '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.'
  146. );
  147. expect(rootCause?.culprit).toEqual(
  148. '/^android\\.app\\.SharedPreferencesImpl\\$EditorImpl\\$[0-9]/'
  149. );
  150. });
  151. it('regex matches function names', function () {
  152. const event = makeEventWithFrames([
  153. {
  154. filename: 'sqlite.SQLiteConnection',
  155. absPath: 'sqlite.SQLiteConnection',
  156. module: 'android.database.sqlite.SQLiteConnection',
  157. package: null,
  158. platform: null,
  159. instructionAddr: null,
  160. symbolAddr: null,
  161. function: 'nativeBindArgs',
  162. rawFunction: null,
  163. symbol: null,
  164. context: [],
  165. lineNo: 366,
  166. colNo: null,
  167. inApp: false,
  168. trust: null,
  169. errors: null,
  170. vars: null,
  171. },
  172. ]);
  173. const rootCause = analyzeFramesForRootCause(event);
  174. expect(rootCause?.resources).toEqual(
  175. '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.'
  176. );
  177. expect(rootCause?.culprit).toEqual('android.database.sqlite.SQLiteConnection');
  178. });
  179. it('picks anr root cause of the topmost frame', function () {
  180. const event = makeEventWithFrames([
  181. {
  182. filename: 'Instrumentation.java',
  183. absPath: 'Instrumentation.java',
  184. module: 'android.app.Instrumentation',
  185. package: null,
  186. platform: null,
  187. instructionAddr: null,
  188. symbolAddr: null,
  189. function: 'callApplicationOnCreate',
  190. rawFunction: null,
  191. symbol: null,
  192. context: [],
  193. lineNo: 1176,
  194. colNo: null,
  195. inApp: false,
  196. trust: null,
  197. errors: null,
  198. vars: null,
  199. },
  200. {
  201. filename: 'SharedPreferencesImpl.java',
  202. absPath: 'SharedPreferencesImpl.java',
  203. module: 'android.app.SharedPreferencesImpl$EditorImpl$1',
  204. package: null,
  205. platform: null,
  206. instructionAddr: null,
  207. symbolAddr: null,
  208. function: 'run',
  209. rawFunction: null,
  210. symbol: null,
  211. context: [],
  212. lineNo: 366,
  213. colNo: null,
  214. inApp: false,
  215. trust: null,
  216. errors: null,
  217. vars: null,
  218. },
  219. ]);
  220. const rootCause = analyzeFramesForRootCause(event);
  221. expect(rootCause?.resources).toEqual(
  222. '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.'
  223. );
  224. expect(rootCause?.culprit).toEqual(
  225. '/^android\\.app\\.SharedPreferencesImpl\\$EditorImpl\\$[0-9]/'
  226. );
  227. });
  228. it('given lock address returns frame with matching lock address', function () {
  229. const frame1 = {
  230. filename: 'Instrumentation.java',
  231. absPath: 'Instrumentation.java',
  232. module: 'android.app.Instrumentation',
  233. package: null,
  234. platform: null,
  235. instructionAddr: null,
  236. symbolAddr: null,
  237. function: 'call',
  238. rawFunction: null,
  239. symbol: null,
  240. context: [],
  241. lineNo: 1176,
  242. colNo: null,
  243. inApp: false,
  244. trust: null,
  245. errors: null,
  246. vars: null,
  247. lock: {
  248. type: LockType.BLOCKED,
  249. address: '0x08a8651c',
  250. package_name: 'io.sentry.samples',
  251. class_name: 'Monitor',
  252. thread_id: 12,
  253. },
  254. };
  255. const frame2 = {
  256. filename: 'MainActivity.java',
  257. absPath: 'MainActivity.java',
  258. module: 'com.example.MainActivity',
  259. package: null,
  260. platform: null,
  261. instructionAddr: null,
  262. symbolAddr: null,
  263. function: 'onCreate',
  264. rawFunction: null,
  265. symbol: null,
  266. context: [],
  267. lineNo: 128,
  268. colNo: null,
  269. inApp: false,
  270. trust: null,
  271. errors: null,
  272. vars: null,
  273. lock: {
  274. type: LockType.BLOCKED,
  275. address: '0x07d7437b',
  276. package_name: 'java.lang',
  277. class_name: 'Object',
  278. thread_id: 7,
  279. },
  280. };
  281. const rootCause1 = analyzeFrameForRootCause(frame1, undefined, '<address>');
  282. expect(rootCause1).toBeNull();
  283. const rootCause2 = analyzeFrameForRootCause(frame2, undefined, '0x07d7437b');
  284. render(<div>{rootCause2?.resources}</div>);
  285. expect(
  286. screen.getByText(
  287. textWithMarkupMatcher(
  288. 'The main thread is blocked/waiting, trying to acquire lock 0x07d7437b (java.lang.Object) held by the suspect frame of this thread.'
  289. )
  290. )
  291. ).toBeInTheDocument();
  292. });
  293. it('when thread id is not provided, does not append "held by"', function () {
  294. const frame = {
  295. filename: 'MainActivity.java',
  296. absPath: 'MainActivity.java',
  297. module: 'com.example.MainActivity',
  298. package: null,
  299. platform: null,
  300. instructionAddr: null,
  301. symbolAddr: null,
  302. function: 'onCreate',
  303. rawFunction: null,
  304. symbol: null,
  305. context: [],
  306. lineNo: 128,
  307. colNo: null,
  308. inApp: false,
  309. trust: null,
  310. errors: null,
  311. vars: null,
  312. lock: {
  313. type: LockType.BLOCKED,
  314. address: '0x07d7437b',
  315. package_name: 'java.lang',
  316. class_name: 'Object',
  317. },
  318. };
  319. const rootCause2 = analyzeFrameForRootCause(frame, undefined, '0x07d7437b');
  320. render(<div>{rootCause2?.resources}</div>);
  321. expect(
  322. screen.getByText(
  323. textWithMarkupMatcher(
  324. 'The main thread is blocked/waiting, trying to acquire lock 0x07d7437b (java.lang.Object) .'
  325. )
  326. )
  327. ).toBeInTheDocument();
  328. });
  329. it('given main thread is locked returns it as root cause', function () {
  330. const frame = {
  331. filename: 'MainActivity.java',
  332. absPath: 'MainActivity.java',
  333. module: 'com.example.MainActivity',
  334. package: null,
  335. platform: null,
  336. instructionAddr: null,
  337. symbolAddr: null,
  338. function: 'onCreate',
  339. rawFunction: null,
  340. symbol: null,
  341. context: [],
  342. lineNo: 128,
  343. colNo: null,
  344. inApp: false,
  345. trust: null,
  346. errors: null,
  347. vars: null,
  348. lock: {
  349. type: LockType.BLOCKED,
  350. address: '0x08a1321b',
  351. package_name: 'java.lang',
  352. class_name: 'Object',
  353. thread_id: 7,
  354. },
  355. };
  356. const thread = {
  357. id: 13920,
  358. current: true,
  359. crashed: true,
  360. name: 'puma 002',
  361. stacktrace: null,
  362. rawStacktrace: null,
  363. state: 'BLOCKED',
  364. };
  365. const rootCause = analyzeFrameForRootCause(frame, thread);
  366. render(<div>{rootCause?.resources}</div>);
  367. expect(
  368. screen.getByText(
  369. textWithMarkupMatcher(
  370. 'The main thread is blocked/waiting, trying to acquire lock 0x08a1321b (java.lang.Object) held by the suspect frame of this thread.'
  371. )
  372. )
  373. ).toBeInTheDocument();
  374. });
  375. });