importProfile.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import * as Sentry from '@sentry/react';
  2. import {Transaction} from '@sentry/types';
  3. import {
  4. isChromeTraceArrayFormat,
  5. isChromeTraceFormat,
  6. isChromeTraceObjectFormat,
  7. isEventedProfile,
  8. isJSProfile,
  9. isSampledProfile,
  10. isSchema,
  11. } from '../guards/profile';
  12. import {parseChromeTraceArrayFormat} from './chromeTraceProfile';
  13. import {EventedProfile} from './eventedProfile';
  14. import {JSSelfProfile} from './jsSelfProfile';
  15. import {Profile} from './profile';
  16. import {SampledProfile} from './sampledProfile';
  17. import {createFrameIndex, wrapWithSpan} from './utils';
  18. export interface ImportOptions {
  19. transaction: Transaction | undefined;
  20. }
  21. export interface ProfileGroup {
  22. activeProfileIndex: number;
  23. name: string;
  24. profiles: Profile[];
  25. traceID: string;
  26. }
  27. export function importProfile(
  28. input: Profiling.Schema | JSSelfProfiling.Trace | ChromeTrace.ProfileType,
  29. traceID: string
  30. ): ProfileGroup {
  31. const transaction = Sentry.startTransaction({
  32. op: 'import',
  33. name: 'profiles.import',
  34. });
  35. try {
  36. if (isJSProfile(input)) {
  37. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  38. if (transaction) {
  39. transaction.setTag('profile.type', 'js-self-profile');
  40. }
  41. return importJSSelfProfile(input, traceID, {transaction});
  42. }
  43. if (isChromeTraceFormat(input)) {
  44. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  45. if (transaction) {
  46. transaction.setTag('profile.type', 'chrometrace');
  47. }
  48. return importChromeTrace(input, traceID, {transaction});
  49. }
  50. if (isSchema(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', 'schema');
  54. }
  55. return importSchema(input, traceID, {transaction});
  56. }
  57. throw new Error('Unsupported trace format');
  58. } catch (error) {
  59. if (transaction) {
  60. transaction.setStatus('internal_error');
  61. }
  62. throw error;
  63. } finally {
  64. if (transaction) {
  65. transaction.finish();
  66. }
  67. }
  68. }
  69. function importJSSelfProfile(
  70. input: JSSelfProfiling.Trace,
  71. traceID: string,
  72. options: ImportOptions
  73. ): ProfileGroup {
  74. const frameIndex = createFrameIndex(input.frames);
  75. return {
  76. traceID,
  77. name: traceID,
  78. activeProfileIndex: 0,
  79. profiles: [importSingleProfile(input, frameIndex, options)],
  80. };
  81. }
  82. function importChromeTrace(
  83. input: ChromeTrace.ProfileType,
  84. traceID: string,
  85. options: ImportOptions
  86. ): ProfileGroup {
  87. if (isChromeTraceObjectFormat(input)) {
  88. throw new Error('Chrometrace object format is not yet supported');
  89. }
  90. if (isChromeTraceArrayFormat(input)) {
  91. return parseChromeTraceArrayFormat(input, traceID, options);
  92. }
  93. throw new Error('Failed to parse trace input format');
  94. }
  95. function importSchema(
  96. input: Profiling.Schema,
  97. traceID: string,
  98. options: ImportOptions
  99. ): ProfileGroup {
  100. const frameIndex = createFrameIndex(input.shared.frames);
  101. return {
  102. traceID,
  103. name: input.transactionName,
  104. activeProfileIndex: input.activeProfileIndex ?? 0,
  105. profiles: input.profiles.map(profile =>
  106. importSingleProfile(profile, frameIndex, options)
  107. ),
  108. };
  109. }
  110. function importSingleProfile(
  111. profile: Profiling.ProfileTypes,
  112. frameIndex: ReturnType<typeof createFrameIndex>,
  113. {transaction}: ImportOptions
  114. ): Profile {
  115. if (isEventedProfile(profile)) {
  116. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  117. if (!transaction) {
  118. return EventedProfile.FromProfile(profile, frameIndex);
  119. }
  120. return wrapWithSpan(
  121. transaction,
  122. () => EventedProfile.FromProfile(profile, frameIndex),
  123. {
  124. op: 'profile.import',
  125. description: 'evented',
  126. }
  127. );
  128. }
  129. if (isSampledProfile(profile)) {
  130. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  131. if (!transaction) {
  132. return SampledProfile.FromProfile(profile, frameIndex);
  133. }
  134. return wrapWithSpan(
  135. transaction,
  136. () => SampledProfile.FromProfile(profile, frameIndex),
  137. {
  138. op: 'profile.import',
  139. description: 'sampled',
  140. }
  141. );
  142. }
  143. if (isJSProfile(profile)) {
  144. // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
  145. if (!transaction) {
  146. return JSSelfProfile.FromProfile(profile, createFrameIndex(profile.frames));
  147. }
  148. return wrapWithSpan(
  149. transaction,
  150. () => JSSelfProfile.FromProfile(profile, createFrameIndex(profile.frames)),
  151. {
  152. op: 'profile.import',
  153. description: 'js-self-profile',
  154. }
  155. );
  156. }
  157. throw new Error('Unrecognized trace format');
  158. }
  159. const tryParseInputString: JSONParser = input => {
  160. try {
  161. return [JSON.parse(input), null];
  162. } catch (e) {
  163. return [null, e];
  164. }
  165. };
  166. type JSONParser = (input: string) => [any, null] | [null, Error];
  167. const TRACE_JSON_PARSERS: ((string) => ReturnType<JSONParser>)[] = [
  168. (input: string) => tryParseInputString(input),
  169. (input: string) => tryParseInputString(input + ']'),
  170. ];
  171. function readFileAsString(file: File): Promise<string> {
  172. return new Promise((resolve, reject) => {
  173. const reader = new FileReader();
  174. reader.addEventListener('load', (e: ProgressEvent<FileReader>) => {
  175. if (typeof e.target?.result === 'string') {
  176. resolve(e.target.result);
  177. return;
  178. }
  179. reject('Failed to read string contents of input file');
  180. });
  181. reader.addEventListener('error', () => {
  182. reject('Failed to read string contents of input file');
  183. });
  184. reader.readAsText(file);
  185. });
  186. }
  187. export async function importDroppedProfile(
  188. file: File,
  189. parsers: JSONParser[] = TRACE_JSON_PARSERS
  190. ): Promise<ProfileGroup> {
  191. const fileContents = await readFileAsString(file);
  192. for (const parser of parsers) {
  193. const [json] = parser(fileContents);
  194. if (json) {
  195. if (typeof json !== 'object' || json === null) {
  196. throw new TypeError('Input JSON is not an object');
  197. }
  198. return importProfile(json, file.name);
  199. }
  200. }
  201. throw new Error('Failed to parse input JSON');
  202. }