utils.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import * as Sentry from '@sentry/react';
  2. import partition from 'lodash/partition';
  3. import * as qs from 'query-string';
  4. import getThreadException from 'sentry/components/events/interfaces/threads/threadSelector/getThreadException';
  5. import {FILTER_MASK} from 'sentry/constants';
  6. import ConfigStore from 'sentry/stores/configStore';
  7. import type {Frame, PlatformKey, StacktraceType} from 'sentry/types';
  8. import type {Image} from 'sentry/types/debugImage';
  9. import type {EntryRequest, EntryThreads, Event, Thread} from 'sentry/types/event';
  10. import {EntryType} from 'sentry/types/event';
  11. import {defined} from 'sentry/utils';
  12. import {fileExtensionToPlatform, getFileExtension} from 'sentry/utils/fileExtension';
  13. /**
  14. * Attempts to escape a string from any bash double quote special characters.
  15. */
  16. function escapeBashString(v: string) {
  17. return v.replace(/(["$`\\])/g, '\\$1');
  18. }
  19. interface ImageForAddressProps {
  20. addrMode: Frame['addrMode'];
  21. address: Frame['instructionAddr'];
  22. event: Event;
  23. }
  24. interface HiddenFrameIndicesProps {
  25. data: StacktraceType;
  26. frameCountMap: {[frameIndex: number]: number};
  27. toggleFrameMap: {[frameIndex: number]: boolean};
  28. }
  29. export function findImageForAddress({event, addrMode, address}: ImageForAddressProps) {
  30. const images = event.entries.find(entry => entry.type === 'debugmeta')?.data?.images;
  31. if (!images || !address) {
  32. return null;
  33. }
  34. const image = images.find((img, idx) => {
  35. if (!addrMode || addrMode === 'abs') {
  36. const [startAddress, endAddress] = getImageRange(img);
  37. return address >= (startAddress as any) && address < (endAddress as any);
  38. }
  39. return addrMode === `rel:${idx}`;
  40. });
  41. return image;
  42. }
  43. export function isRepeatedFrame(frame: Frame, nextFrame?: Frame) {
  44. if (!nextFrame) {
  45. return false;
  46. }
  47. return (
  48. frame.lineNo === nextFrame.lineNo &&
  49. frame.instructionAddr === nextFrame.instructionAddr &&
  50. frame.package === nextFrame.package &&
  51. frame.module === nextFrame.module &&
  52. frame.function === nextFrame.function
  53. );
  54. }
  55. export function getRepeatedFrameIndices(data: StacktraceType) {
  56. const repeats: number[] = [];
  57. (data.frames ?? []).forEach((frame, frameIdx) => {
  58. const nextFrame = (data.frames ?? [])[frameIdx + 1];
  59. const repeatedFrame = isRepeatedFrame(frame, nextFrame);
  60. if (repeatedFrame) {
  61. repeats.push(frameIdx);
  62. }
  63. });
  64. return repeats;
  65. }
  66. export function getHiddenFrameIndices({
  67. data,
  68. toggleFrameMap,
  69. frameCountMap,
  70. }: HiddenFrameIndicesProps) {
  71. const repeatedIndeces = getRepeatedFrameIndices(data);
  72. let hiddenFrameIndices: number[] = [];
  73. Object.keys(toggleFrameMap)
  74. .filter(frameIndex => toggleFrameMap[frameIndex] === true)
  75. .forEach(indexString => {
  76. const index = parseInt(indexString, 10);
  77. const indicesToBeAdded: number[] = [];
  78. let i = 1;
  79. let numHidden = frameCountMap[index];
  80. while (numHidden > 0) {
  81. if (!repeatedIndeces.includes(index - i)) {
  82. indicesToBeAdded.push(index - i);
  83. numHidden -= 1;
  84. }
  85. i += 1;
  86. }
  87. hiddenFrameIndices = [...hiddenFrameIndices, ...indicesToBeAdded];
  88. });
  89. return hiddenFrameIndices;
  90. }
  91. export function getLastFrameIndex(frames: Frame[]) {
  92. const inAppFrameIndexes = frames
  93. .map((frame, frameIndex) => {
  94. if (frame.inApp) {
  95. return frameIndex;
  96. }
  97. return undefined;
  98. })
  99. .filter(frame => frame !== undefined);
  100. return !inAppFrameIndexes.length
  101. ? frames.length - 1
  102. : inAppFrameIndexes[inAppFrameIndexes.length - 1];
  103. }
  104. // TODO(dcramer): support cookies
  105. export function getCurlCommand(data: EntryRequest['data']) {
  106. let result = 'curl';
  107. if (defined(data.method) && data.method !== 'GET') {
  108. result += ' \\\n -X ' + data.method;
  109. }
  110. data.headers = data.headers?.filter(defined);
  111. // TODO(benvinegar): just gzip? what about deflate?
  112. const compressed = data.headers?.find(
  113. h => h[0] === 'Accept-Encoding' && h[1].includes('gzip')
  114. );
  115. if (compressed) {
  116. result += ' \\\n --compressed';
  117. }
  118. // sort headers
  119. const headers =
  120. data.headers?.sort(function (a, b) {
  121. return a[0] === b[0] ? 0 : a[0] < b[0] ? -1 : 1;
  122. }) ?? [];
  123. for (const header of headers) {
  124. result += ' \\\n -H "' + header[0] + ': ' + escapeBashString(header[1] + '') + '"';
  125. }
  126. if (defined(data.data)) {
  127. switch (data.inferredContentType) {
  128. case 'application/json':
  129. result += ' \\\n --data "' + escapeBashString(JSON.stringify(data.data)) + '"';
  130. break;
  131. case 'application/x-www-form-urlencoded':
  132. result +=
  133. ' \\\n --data "' +
  134. escapeBashString(qs.stringify(data.data as {[key: string]: any})) +
  135. '"';
  136. break;
  137. default:
  138. if (typeof data.data === 'string') {
  139. result += ' \\\n --data "' + escapeBashString(data.data) + '"';
  140. } else if (Object.keys(data.data).length === 0) {
  141. // Do nothing with empty object data.
  142. } else {
  143. Sentry.withScope(scope => {
  144. scope.setExtra('data', data);
  145. Sentry.captureException(new Error('Unknown event data'));
  146. });
  147. }
  148. }
  149. }
  150. result += ' \\\n "' + getFullUrl(data) + '"';
  151. return result;
  152. }
  153. export function stringifyQueryList(query: string | [key: string, value: string][]) {
  154. if (typeof query === 'string') {
  155. return query;
  156. }
  157. const queryObj: Record<string, string[]> = {};
  158. for (const kv of query) {
  159. if (kv !== null && kv.length === 2) {
  160. const [key, value] = kv;
  161. if (value !== null) {
  162. if (Array.isArray(queryObj[key])) {
  163. queryObj[key].push(value);
  164. } else {
  165. queryObj[key] = [value];
  166. }
  167. }
  168. }
  169. }
  170. return qs.stringify(queryObj);
  171. }
  172. export function getFullUrl(data: EntryRequest['data']): string | undefined {
  173. let fullUrl = data?.url;
  174. if (!fullUrl) {
  175. return fullUrl;
  176. }
  177. if (data?.query?.length) {
  178. fullUrl += '?' + stringifyQueryList(data.query);
  179. }
  180. if (data.fragment) {
  181. fullUrl += '#' + data.fragment;
  182. }
  183. return escapeBashString(fullUrl);
  184. }
  185. /**
  186. * Converts an object of body/querystring key/value pairs
  187. * into a tuple of [key, value] pairs, and sorts them.
  188. *
  189. * This handles the case for query strings that were decoded like so:
  190. *
  191. * ?foo=bar&foo=baz => { foo: ['bar', 'baz'] }
  192. *
  193. * By converting them to [['foo', 'bar'], ['foo', 'baz']]
  194. */
  195. export function objectToSortedTupleArray(obj: Record<string, string | string[]>) {
  196. return Object.keys(obj)
  197. .reduce<[string, string][]>((out, k) => {
  198. const val = obj[k];
  199. return out.concat(
  200. Array.isArray(val)
  201. ? val.map(v => [k, v]) // key has multiple values (array)
  202. : ([[k, val]] as [string, string][]) // key has single value
  203. );
  204. }, [])
  205. .sort(function ([keyA, valA], [keyB, valB]) {
  206. // if keys are identical, sort on value
  207. if (keyA === keyB) {
  208. return valA < valB ? -1 : 1;
  209. }
  210. return keyA < keyB ? -1 : 1;
  211. });
  212. }
  213. // for context summaries and avatars
  214. export function removeFilterMaskedEntries<T extends Record<string, any>>(rawData: T): T {
  215. const cleanedData: Record<string, any> = {};
  216. for (const key of Object.getOwnPropertyNames(rawData)) {
  217. if (rawData[key] !== FILTER_MASK) {
  218. cleanedData[key] = rawData[key];
  219. }
  220. }
  221. return cleanedData as T;
  222. }
  223. export function formatAddress(address: number, imageAddressLength: number | undefined) {
  224. return `0x${address.toString(16).padStart(imageAddressLength ?? 0, '0')}`;
  225. }
  226. export function parseAddress(address?: string | null) {
  227. if (!address) {
  228. return 0;
  229. }
  230. try {
  231. return parseInt(address, 16) || 0;
  232. } catch (_e) {
  233. return 0;
  234. }
  235. }
  236. export function getImageRange(image: Image) {
  237. // The start address is normalized to a `0x` prefixed hex string. The event
  238. // schema also allows ingesting plain numbers, but this is converted during
  239. // ingestion.
  240. const startAddress = parseAddress(image?.image_addr);
  241. // The image size is normalized to a regular number. However, it can also be
  242. // `null`, in which case we assume that it counts up to the next image.
  243. const endAddress = startAddress + (image?.image_size || 0);
  244. return [startAddress, endAddress];
  245. }
  246. export function parseAssembly(assembly: string | null) {
  247. let name: string | undefined;
  248. let version: string | undefined;
  249. let culture: string | undefined;
  250. let publicKeyToken: string | undefined;
  251. const pieces = assembly ? assembly.split(',') : [];
  252. if (pieces.length > 0) {
  253. name = pieces[0];
  254. }
  255. for (let i = 1; i < pieces.length; i++) {
  256. const [key, value] = pieces[i].trim().split('=');
  257. // eslint-disable-next-line default-case
  258. switch (key) {
  259. case 'Version':
  260. version = value;
  261. break;
  262. case 'Culture':
  263. if (value !== 'neutral') {
  264. culture = value;
  265. }
  266. break;
  267. case 'PublicKeyToken':
  268. if (value !== 'null') {
  269. publicKeyToken = value;
  270. }
  271. break;
  272. }
  273. }
  274. return {name, version, culture, publicKeyToken};
  275. }
  276. function getFramePlatform(frame: Frame) {
  277. const fileExtension = getFileExtension(frame.filename ?? '');
  278. const fileExtensionPlatform = fileExtension
  279. ? fileExtensionToPlatform(fileExtension)
  280. : null;
  281. if (fileExtensionPlatform) {
  282. return fileExtensionPlatform;
  283. }
  284. if (frame.platform) {
  285. return frame.platform;
  286. }
  287. return null;
  288. }
  289. /**
  290. * Returns the representative platform for the given stack trace frames.
  291. * Prioritizes recent in-app frames, checking first for a matching file extension
  292. * and then for a frame.platform attribute [1].
  293. *
  294. * If none of the frames have a platform, falls back to the event platform.
  295. *
  296. * [1] https://develop.sentry.dev/sdk/event-payloads/stacktrace/#frame-attributes
  297. */
  298. export function stackTracePlatformIcon(eventPlatform: PlatformKey, frames: Frame[]) {
  299. const [inAppFrames, systemFrames] = partition(
  300. // Reverse frames to get newest-first ordering
  301. [...frames].reverse(),
  302. frame => frame.inApp
  303. );
  304. for (const frame of [...inAppFrames, ...systemFrames]) {
  305. const framePlatform = getFramePlatform(frame);
  306. if (framePlatform) {
  307. return framePlatform;
  308. }
  309. }
  310. return eventPlatform;
  311. }
  312. export function isStacktraceNewestFirst() {
  313. const user = ConfigStore.get('user');
  314. // user may not be authenticated
  315. if (!user) {
  316. return true;
  317. }
  318. switch (user.options.stacktraceOrder) {
  319. case 2:
  320. return true;
  321. case 1:
  322. return false;
  323. case -1:
  324. default:
  325. return true;
  326. }
  327. }
  328. export function getCurrentThread(event: Event) {
  329. const threads = event.entries?.find(entry => entry.type === EntryType.THREADS) as
  330. | EntryThreads
  331. | undefined;
  332. return threads?.data.values?.find(thread => thread.current);
  333. }
  334. export function getThreadById(event: Event, tid?: number) {
  335. const threads = event.entries?.find(entry => entry.type === EntryType.THREADS) as
  336. | EntryThreads
  337. | undefined;
  338. return threads?.data.values?.find(thread => thread.id === tid);
  339. }
  340. export function getStacktracePlatform(
  341. event: Event,
  342. stacktrace?: StacktraceType | null
  343. ): PlatformKey {
  344. const overridePlatform = stacktrace?.frames?.find(frame =>
  345. defined(frame.platform)
  346. )?.platform;
  347. return overridePlatform ?? event.platform ?? 'other';
  348. }
  349. export function inferPlatform(event: Event, thread?: Thread): PlatformKey {
  350. const exception = getThreadException(event, thread);
  351. let exceptionFramePlatform: Frame | undefined = undefined;
  352. for (const value of exception?.values ?? []) {
  353. exceptionFramePlatform = value.stacktrace?.frames?.find(frame => !!frame.platform);
  354. if (exceptionFramePlatform) {
  355. break;
  356. }
  357. }
  358. if (exceptionFramePlatform?.platform) {
  359. return exceptionFramePlatform.platform;
  360. }
  361. const threadFramePlatform = thread?.stacktrace?.frames?.find(frame => !!frame.platform);
  362. if (threadFramePlatform?.platform) {
  363. return threadFramePlatform.platform;
  364. }
  365. return event.platform ?? 'other';
  366. }