utils.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import * as Sentry from '@sentry/react';
  2. import compact from 'lodash/compact';
  3. import isString from 'lodash/isString';
  4. import uniq from 'lodash/uniq';
  5. import * as qs from 'query-string';
  6. import {FILTER_MASK} from 'sentry/constants';
  7. import ConfigStore from 'sentry/stores/configStore';
  8. import {Frame, PlatformType} from 'sentry/types';
  9. import {Image} from 'sentry/types/debugImage';
  10. import {EntryRequest} from 'sentry/types/event';
  11. import {defined} from 'sentry/utils';
  12. import {fileExtensionToPlatform, getFileExtension} from 'sentry/utils/fileExtension';
  13. export function escapeQuotes(v: string) {
  14. return v.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  15. }
  16. // TODO(dcramer): support cookies
  17. export function getCurlCommand(data: EntryRequest['data']) {
  18. let result = 'curl';
  19. if (defined(data.method) && data.method !== 'GET') {
  20. result += ' \\\n -X ' + data.method;
  21. }
  22. // TODO(benvinegar): just gzip? what about deflate?
  23. const compressed = data.headers?.find(
  24. h => h[0] === 'Accept-Encoding' && h[1].indexOf('gzip') !== -1
  25. );
  26. if (compressed) {
  27. result += ' \\\n --compressed';
  28. }
  29. // sort headers
  30. const headers =
  31. data.headers?.sort(function (a, b) {
  32. return a[0] === b[0] ? 0 : a[0] < b[0] ? -1 : 1;
  33. }) ?? [];
  34. for (const header of headers) {
  35. result += ' \\\n -H "' + header[0] + ': ' + escapeQuotes(header[1] + '') + '"';
  36. }
  37. if (defined(data.data)) {
  38. switch (data.inferredContentType) {
  39. case 'application/json':
  40. result += ' \\\n --data "' + escapeQuotes(JSON.stringify(data.data)) + '"';
  41. break;
  42. case 'application/x-www-form-urlencoded':
  43. result +=
  44. ' \\\n --data "' +
  45. escapeQuotes(qs.stringify(data.data as {[key: string]: any})) +
  46. '"';
  47. break;
  48. default:
  49. if (isString(data.data)) {
  50. result += ' \\\n --data "' + escapeQuotes(data.data) + '"';
  51. } else if (Object.keys(data.data).length === 0) {
  52. // Do nothing with empty object data.
  53. } else {
  54. Sentry.withScope(scope => {
  55. scope.setExtra('data', data);
  56. Sentry.captureException(new Error('Unknown event data'));
  57. });
  58. }
  59. }
  60. }
  61. result += ' \\\n "' + getFullUrl(data) + '"';
  62. return result;
  63. }
  64. export function stringifyQueryList(query: string | [key: string, value: string][]) {
  65. if (isString(query)) {
  66. return query;
  67. }
  68. const queryObj: Record<string, string[]> = {};
  69. for (const kv of query) {
  70. if (kv !== null && kv.length === 2) {
  71. const [key, value] = kv;
  72. if (value !== null) {
  73. if (Array.isArray(queryObj[key])) {
  74. queryObj[key].push(value);
  75. } else {
  76. queryObj[key] = [value];
  77. }
  78. }
  79. }
  80. }
  81. return qs.stringify(queryObj);
  82. }
  83. export function getFullUrl(data: EntryRequest['data']): string | undefined {
  84. let fullUrl = data?.url;
  85. if (!fullUrl) {
  86. return fullUrl;
  87. }
  88. if (data?.query?.length) {
  89. fullUrl += '?' + stringifyQueryList(data.query);
  90. }
  91. if (data.fragment) {
  92. fullUrl += '#' + data.fragment;
  93. }
  94. return fullUrl;
  95. }
  96. /**
  97. * Converts an object of body/querystring key/value pairs
  98. * into a tuple of [key, value] pairs, and sorts them.
  99. *
  100. * This handles the case for query strings that were decoded like so:
  101. *
  102. * ?foo=bar&foo=baz => { foo: ['bar', 'baz'] }
  103. *
  104. * By converting them to [['foo', 'bar'], ['foo', 'baz']]
  105. */
  106. export function objectToSortedTupleArray(obj: Record<string, string | string[]>) {
  107. return Object.keys(obj)
  108. .reduce<[string, string][]>((out, k) => {
  109. const val = obj[k];
  110. return out.concat(
  111. Array.isArray(val)
  112. ? val.map(v => [k, v]) // key has multiple values (array)
  113. : ([[k, val]] as [string, string][]) // key has single value
  114. );
  115. }, [])
  116. .sort(function ([keyA, valA], [keyB, valB]) {
  117. // if keys are identical, sort on value
  118. if (keyA === keyB) {
  119. return valA < valB ? -1 : 1;
  120. }
  121. return keyA < keyB ? -1 : 1;
  122. });
  123. }
  124. // for context summaries and avatars
  125. export function removeFilterMaskedEntries<T extends Record<string, any>>(rawData: T): T {
  126. const cleanedData: Record<string, any> = {};
  127. for (const key of Object.getOwnPropertyNames(rawData)) {
  128. if (rawData[key] !== FILTER_MASK) {
  129. cleanedData[key] = rawData[key];
  130. }
  131. }
  132. return cleanedData as T;
  133. }
  134. export function formatAddress(address: number, imageAddressLength: number | undefined) {
  135. return `0x${address.toString(16).padStart(imageAddressLength ?? 0, '0')}`;
  136. }
  137. export function parseAddress(address?: string | null) {
  138. if (!address) {
  139. return 0;
  140. }
  141. try {
  142. return parseInt(address, 16) || 0;
  143. } catch (_e) {
  144. return 0;
  145. }
  146. }
  147. export function getImageRange(image: Image) {
  148. // The start address is normalized to a `0x` prefixed hex string. The event
  149. // schema also allows ingesting plain numbers, but this is converted during
  150. // ingestion.
  151. const startAddress = parseAddress(image?.image_addr);
  152. // The image size is normalized to a regular number. However, it can also be
  153. // `null`, in which case we assume that it counts up to the next image.
  154. const endAddress = startAddress + (image?.image_size || 0);
  155. return [startAddress, endAddress];
  156. }
  157. export function parseAssembly(assembly: string | null) {
  158. let name: string | undefined;
  159. let version: string | undefined;
  160. let culture: string | undefined;
  161. let publicKeyToken: string | undefined;
  162. const pieces = assembly ? assembly.split(',') : [];
  163. if (pieces.length === 4) {
  164. name = pieces[0];
  165. version = pieces[1].split('Version=')[1];
  166. culture = pieces[2].split('Culture=')[1];
  167. publicKeyToken = pieces[3].split('PublicKeyToken=')[1];
  168. }
  169. return {name, version, culture, publicKeyToken};
  170. }
  171. export function stackTracePlatformIcon(platform: PlatformType, frames: Frame[]) {
  172. const fileExtensions = uniq(
  173. compact(frames.map(frame => getFileExtension(frame.filename ?? '')))
  174. );
  175. if (fileExtensions.length === 1) {
  176. const newPlatform = fileExtensionToPlatform(fileExtensions[0]);
  177. return newPlatform ?? platform;
  178. }
  179. return platform;
  180. }
  181. export function isStacktraceNewestFirst() {
  182. const user = ConfigStore.get('user');
  183. // user may not be authenticated
  184. if (!user) {
  185. return true;
  186. }
  187. switch (user.options.stacktraceOrder) {
  188. case 2:
  189. return true;
  190. case 1:
  191. return false;
  192. case -1:
  193. default:
  194. return true;
  195. }
  196. }