importProfile.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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(input.profile.frames);
  106. const samplesByThread: Record<
  107. string,
  108. Profiling.SentrySampledProfile['profile']['samples']
  109. > = {};
  110. for (let i = 0; i < input.profile.samples.length; i++) {
  111. const sample = input.profile.samples[i];
  112. if (!samplesByThread[sample.thread_id]) {
  113. samplesByThread[sample.thread_id] = [];
  114. }
  115. samplesByThread[sample.thread_id].push(sample);
  116. }
  117. for (const key in samplesByThread) {
  118. samplesByThread[key].sort(
  119. (a, b) => a.elapsed_since_start_ns - b.elapsed_since_start_ns
  120. );
  121. }
  122. let activeProfileIndex = 0;
  123. const profiles: Profile[] = [];
  124. for (const key in samplesByThread) {
  125. const profile: Profiling.SentrySampledProfile = {
  126. ...input,
  127. profile: {
  128. ...input.profile,
  129. samples: samplesByThread[key],
  130. },
  131. };
  132. if (key === String(input.transaction.active_thread_id)) {
  133. activeProfileIndex = profiles.length;
  134. }
  135. profiles.push(
  136. wrapWithSpan(
  137. options.transaction,
  138. () =>
  139. SentrySampledProfile.FromProfile(profile, frameIndex, {
  140. type: options.type,
  141. frameFilter: options.frameFilter,
  142. }),
  143. {
  144. op: 'profile.import',
  145. description: 'evented',
  146. }
  147. )
  148. );
  149. }
  150. return {
  151. transactionID: input.transaction.id,
  152. traceID: input.transaction.trace_id,
  153. name: input.transaction.name,
  154. activeProfileIndex,
  155. measurements: input.measurements,
  156. metadata: {
  157. deviceLocale: input.device.locale,
  158. deviceManufacturer: input.device.manufacturer,
  159. deviceModel: input.device.model,
  160. deviceOSName: input.os.name,
  161. deviceOSVersion: input.os.version,
  162. environment: input.environment,
  163. platform: input.platform,
  164. profileID: input.event_id,
  165. projectID: input.project_id,
  166. release: input.release,
  167. received: input.received,
  168. // these don't really work for multiple transactions
  169. transactionID: input.transaction.id,
  170. transactionName: input.transaction.name,
  171. traceID: input.transaction.trace_id,
  172. },
  173. profiles,
  174. images: input.debug_meta?.images,
  175. };
  176. }
  177. export function importSchema(
  178. input: Readonly<Profiling.Schema>,
  179. traceID: string,
  180. options: ImportOptions
  181. ): ProfileGroup {
  182. const frameIndex = createFrameIndex(
  183. input.metadata.platform === 'node'
  184. ? 'node'
  185. : input.metadata.platform === 'javascript'
  186. ? 'javascript'
  187. : 'mobile',
  188. input.shared.frames
  189. );
  190. return {
  191. traceID,
  192. transactionID: input.metadata.transactionID ?? null,
  193. name: input.metadata?.transactionName ?? traceID,
  194. activeProfileIndex: input.activeProfileIndex ?? 0,
  195. metadata: input.metadata ?? {},
  196. measurements: input.measurements ?? {},
  197. profiles: input.profiles.map(profile =>
  198. importSingleProfile(profile, frameIndex, {
  199. ...options,
  200. profileIds: input.shared.profile_ids,
  201. })
  202. ),
  203. };
  204. }
  205. function importSingleProfile(
  206. profile: Profiling.EventedProfile | Profiling.SampledProfile | JSSelfProfiling.Trace,
  207. frameIndex: ReturnType<typeof createFrameIndex>,
  208. {transaction, type, frameFilter, profileIds}: ImportOptions
  209. ): Profile {
  210. if (isEventedProfile(profile)) {
  211. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  212. if (!transaction) {
  213. return EventedProfile.FromProfile(profile, frameIndex, {type, frameFilter});
  214. }
  215. return wrapWithSpan(
  216. transaction,
  217. () => EventedProfile.FromProfile(profile, frameIndex, {type, frameFilter}),
  218. {
  219. op: 'profile.import',
  220. description: 'evented',
  221. }
  222. );
  223. }
  224. if (isSampledProfile(profile)) {
  225. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  226. if (!transaction) {
  227. return SampledProfile.FromProfile(profile, frameIndex, {
  228. type,
  229. frameFilter,
  230. profileIds,
  231. });
  232. }
  233. return wrapWithSpan(
  234. transaction,
  235. () =>
  236. SampledProfile.FromProfile(profile, frameIndex, {type, frameFilter, profileIds}),
  237. {
  238. op: 'profile.import',
  239. description: 'sampled',
  240. }
  241. );
  242. }
  243. if (isJSProfile(profile)) {
  244. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  245. if (!transaction) {
  246. return JSSelfProfile.FromProfile(
  247. profile,
  248. createFrameIndex('javascript', profile.frames),
  249. {
  250. type,
  251. }
  252. );
  253. }
  254. return wrapWithSpan(
  255. transaction,
  256. () =>
  257. JSSelfProfile.FromProfile(
  258. profile,
  259. createFrameIndex('javascript', profile.frames),
  260. {
  261. type,
  262. }
  263. ),
  264. {
  265. op: 'profile.import',
  266. description: 'js-self-profile',
  267. }
  268. );
  269. }
  270. throw new Error('Unrecognized trace format');
  271. }
  272. const tryParseInputString: JSONParser = input => {
  273. try {
  274. return [JSON.parse(input), null];
  275. } catch (e) {
  276. return [null, e];
  277. }
  278. };
  279. type JSONParser = (input: string) => [any, null] | [null, Error];
  280. const TRACE_JSON_PARSERS: ((string) => ReturnType<JSONParser>)[] = [
  281. (input: string) => tryParseInputString(input),
  282. (input: string) => tryParseInputString(input + ']'),
  283. ];
  284. function readFileAsString(file: File): Promise<string> {
  285. return new Promise((resolve, reject) => {
  286. const reader = new FileReader();
  287. reader.addEventListener('load', (e: ProgressEvent<FileReader>) => {
  288. if (typeof e.target?.result === 'string') {
  289. resolve(e.target.result);
  290. return;
  291. }
  292. reject('Failed to read string contents of input file');
  293. });
  294. reader.addEventListener('error', () => {
  295. reject('Failed to read string contents of input file');
  296. });
  297. reader.readAsText(file);
  298. });
  299. }
  300. export async function parseDroppedProfile(
  301. file: File,
  302. parsers: JSONParser[] = TRACE_JSON_PARSERS
  303. ): Promise<Profiling.ProfileInput> {
  304. const fileContents = await readFileAsString(file);
  305. for (const parser of parsers) {
  306. const [json] = parser(fileContents);
  307. if (json) {
  308. if (typeof json !== 'object' || json === null) {
  309. throw new TypeError('Input JSON is not an object');
  310. }
  311. return json;
  312. }
  313. }
  314. throw new Error('Failed to parse input JSON');
  315. }