importProfile.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import * as Sentry from '@sentry/react';
  2. import {Transaction} from '@sentry/types';
  3. import {Image} from 'sentry/types/debugImage';
  4. import {Frame} from '../frame';
  5. import {
  6. isEventedProfile,
  7. isJSProfile,
  8. isSampledProfile,
  9. isSchema,
  10. isSentrySampledProfile,
  11. } from '../guards/profile';
  12. import {EventedProfile} from './eventedProfile';
  13. import {JSSelfProfile} from './jsSelfProfile';
  14. import {Profile} from './profile';
  15. import {SampledProfile} from './sampledProfile';
  16. import {SentrySampledProfile} from './sentrySampledProfile';
  17. import {
  18. createFrameIndex,
  19. createSentrySampleProfileFrameIndex,
  20. wrapWithSpan,
  21. } from './utils';
  22. export interface ImportOptions {
  23. transaction: Transaction | undefined;
  24. type: 'flamegraph' | 'flamechart';
  25. frameFilter?: (frame: Frame) => boolean;
  26. profileIds?: Readonly<string[]>;
  27. }
  28. export interface ProfileGroup {
  29. activeProfileIndex: number;
  30. measurements: Partial<Profiling.Schema['measurements']>;
  31. metadata: Partial<Profiling.Schema['metadata']>;
  32. name: string;
  33. profiles: Profile[];
  34. traceID: string;
  35. transactionID: string | null;
  36. images?: Image[];
  37. }
  38. export function importProfile(
  39. input: Readonly<Profiling.ProfileInput>,
  40. traceID: string,
  41. type: 'flamegraph' | 'flamechart',
  42. frameFilter?: (frame: Frame) => boolean
  43. ): ProfileGroup {
  44. const transaction = Sentry.startTransaction({
  45. op: 'import',
  46. name: 'profiles.import',
  47. });
  48. try {
  49. if (isJSProfile(input)) {
  50. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  51. if (transaction) {
  52. transaction.setTag('profile.type', 'js-self-profile');
  53. }
  54. return importJSSelfProfile(input, traceID, {transaction, type});
  55. }
  56. if (isSentrySampledProfile(input)) {
  57. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  58. if (transaction) {
  59. transaction.setTag('profile.type', 'sentry-sampled');
  60. }
  61. return importSentrySampledProfile(input, {transaction, type, frameFilter});
  62. }
  63. if (isSchema(input)) {
  64. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  65. if (transaction) {
  66. transaction.setTag('profile.type', 'schema');
  67. }
  68. return importSchema(input, traceID, {transaction, type, frameFilter});
  69. }
  70. throw new Error('Unsupported trace format');
  71. } catch (error) {
  72. if (transaction) {
  73. transaction.setStatus('internal_error');
  74. }
  75. throw error;
  76. } finally {
  77. if (transaction) {
  78. transaction.finish();
  79. }
  80. }
  81. }
  82. function importJSSelfProfile(
  83. input: Readonly<JSSelfProfiling.Trace>,
  84. traceID: string,
  85. options: ImportOptions
  86. ): ProfileGroup {
  87. const frameIndex = createFrameIndex('javascript', input.frames);
  88. const profile = importSingleProfile(input, frameIndex, options);
  89. return {
  90. traceID,
  91. name: traceID,
  92. transactionID: null,
  93. activeProfileIndex: 0,
  94. profiles: [profile],
  95. measurements: {},
  96. metadata: {
  97. platform: 'javascript',
  98. },
  99. };
  100. }
  101. function importSentrySampledProfile(
  102. input: Readonly<Profiling.SentrySampledProfile>,
  103. options: ImportOptions
  104. ): ProfileGroup {
  105. const frameIndex = createSentrySampleProfileFrameIndex(
  106. input.profile.frames,
  107. input.platform
  108. );
  109. const samplesByThread: Record<
  110. string,
  111. Profiling.SentrySampledProfile['profile']['samples']
  112. > = {};
  113. for (let i = 0; i < input.profile.samples.length; i++) {
  114. const sample = input.profile.samples[i];
  115. if (!samplesByThread[sample.thread_id]) {
  116. samplesByThread[sample.thread_id] = [];
  117. }
  118. samplesByThread[sample.thread_id].push(sample);
  119. }
  120. for (const key in samplesByThread) {
  121. samplesByThread[key].sort(
  122. (a, b) => a.elapsed_since_start_ns - b.elapsed_since_start_ns
  123. );
  124. }
  125. let activeProfileIndex = 0;
  126. const profiles: Profile[] = [];
  127. for (const key in samplesByThread) {
  128. const profile: Profiling.SentrySampledProfile = {
  129. ...input,
  130. profile: {
  131. ...input.profile,
  132. samples: samplesByThread[key],
  133. },
  134. };
  135. if (key === String(input.transaction.active_thread_id)) {
  136. activeProfileIndex = profiles.length;
  137. }
  138. profiles.push(
  139. wrapWithSpan(
  140. options.transaction,
  141. () =>
  142. SentrySampledProfile.FromProfile(profile, frameIndex, {
  143. type: options.type,
  144. frameFilter: options.frameFilter,
  145. }),
  146. {
  147. op: 'profile.import',
  148. description: 'evented',
  149. }
  150. )
  151. );
  152. }
  153. return {
  154. transactionID: input.transaction.id,
  155. traceID: input.transaction.trace_id,
  156. name: input.transaction.name,
  157. activeProfileIndex,
  158. measurements: input.measurements,
  159. metadata: {
  160. deviceLocale: input.device.locale,
  161. deviceManufacturer: input.device.manufacturer,
  162. deviceModel: input.device.model,
  163. deviceOSName: input.os.name,
  164. deviceOSVersion: input.os.version,
  165. environment: input.environment,
  166. platform: input.platform,
  167. profileID: input.event_id,
  168. projectID: input.project_id,
  169. release: input.release,
  170. received: input.received,
  171. // these don't really work for multiple transactions
  172. transactionID: input.transaction.id,
  173. transactionName: input.transaction.name,
  174. traceID: input.transaction.trace_id,
  175. },
  176. profiles,
  177. images: input.debug_meta?.images,
  178. };
  179. }
  180. export function importSchema(
  181. input: Readonly<Profiling.Schema>,
  182. traceID: string,
  183. options: ImportOptions
  184. ): ProfileGroup {
  185. const frameIndex = createFrameIndex(
  186. input.metadata.platform === 'node'
  187. ? 'node'
  188. : input.metadata.platform === 'javascript'
  189. ? 'javascript'
  190. : 'mobile',
  191. input.shared.frames
  192. );
  193. return {
  194. traceID,
  195. transactionID: input.metadata.transactionID ?? null,
  196. name: input.metadata?.transactionName ?? traceID,
  197. activeProfileIndex: input.activeProfileIndex ?? 0,
  198. metadata: input.metadata ?? {},
  199. measurements: input.measurements ?? {},
  200. profiles: input.profiles.map(profile =>
  201. importSingleProfile(profile, frameIndex, {
  202. ...options,
  203. profileIds: input.shared.profile_ids,
  204. })
  205. ),
  206. };
  207. }
  208. function importSingleProfile(
  209. profile: Profiling.EventedProfile | Profiling.SampledProfile | JSSelfProfiling.Trace,
  210. frameIndex: ReturnType<typeof createFrameIndex>,
  211. {transaction, type, frameFilter, profileIds}: ImportOptions
  212. ): Profile {
  213. if (isEventedProfile(profile)) {
  214. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  215. if (!transaction) {
  216. return EventedProfile.FromProfile(profile, frameIndex, {type, frameFilter});
  217. }
  218. return wrapWithSpan(
  219. transaction,
  220. () => EventedProfile.FromProfile(profile, frameIndex, {type, frameFilter}),
  221. {
  222. op: 'profile.import',
  223. description: 'evented',
  224. }
  225. );
  226. }
  227. if (isSampledProfile(profile)) {
  228. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  229. if (!transaction) {
  230. return SampledProfile.FromProfile(profile, frameIndex, {
  231. type,
  232. frameFilter,
  233. profileIds,
  234. });
  235. }
  236. return wrapWithSpan(
  237. transaction,
  238. () =>
  239. SampledProfile.FromProfile(profile, frameIndex, {type, frameFilter, profileIds}),
  240. {
  241. op: 'profile.import',
  242. description: 'sampled',
  243. }
  244. );
  245. }
  246. if (isJSProfile(profile)) {
  247. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  248. if (!transaction) {
  249. return JSSelfProfile.FromProfile(
  250. profile,
  251. createFrameIndex('javascript', profile.frames),
  252. {
  253. type,
  254. }
  255. );
  256. }
  257. return wrapWithSpan(
  258. transaction,
  259. () =>
  260. JSSelfProfile.FromProfile(
  261. profile,
  262. createFrameIndex('javascript', profile.frames),
  263. {
  264. type,
  265. }
  266. ),
  267. {
  268. op: 'profile.import',
  269. description: 'js-self-profile',
  270. }
  271. );
  272. }
  273. throw new Error('Unrecognized trace format');
  274. }
  275. const tryParseInputString: JSONParser = input => {
  276. try {
  277. return [JSON.parse(input), null];
  278. } catch (e) {
  279. return [null, e];
  280. }
  281. };
  282. type JSONParser = (input: string) => [any, null] | [null, Error];
  283. const TRACE_JSON_PARSERS: ((string) => ReturnType<JSONParser>)[] = [
  284. (input: string) => tryParseInputString(input),
  285. (input: string) => tryParseInputString(input + ']'),
  286. ];
  287. function readFileAsString(file: File): Promise<string> {
  288. return new Promise((resolve, reject) => {
  289. const reader = new FileReader();
  290. reader.addEventListener('load', (e: ProgressEvent<FileReader>) => {
  291. if (typeof e.target?.result === 'string') {
  292. resolve(e.target.result);
  293. return;
  294. }
  295. reject('Failed to read string contents of input file');
  296. });
  297. reader.addEventListener('error', () => {
  298. reject('Failed to read string contents of input file');
  299. });
  300. reader.readAsText(file);
  301. });
  302. }
  303. export async function parseDroppedProfile(
  304. file: File,
  305. parsers: JSONParser[] = TRACE_JSON_PARSERS
  306. ): Promise<Profiling.ProfileInput> {
  307. const fileContents = await readFileAsString(file);
  308. for (const parser of parsers) {
  309. const [json] = parser(fileContents);
  310. if (json) {
  311. if (typeof json !== 'object' || json === null) {
  312. throw new TypeError('Input JSON is not an object');
  313. }
  314. return json;
  315. }
  316. }
  317. throw new Error('Failed to parse input JSON');
  318. }