importProfile.tsx 10 KB

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