importProfile.tsx 8.9 KB

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