utils.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
  2. import type {EventTransaction} from 'sentry/types/event';
  3. import {EntryType, EventOrGroupType} from 'sentry/types/event';
  4. import {IssueType} from 'sentry/types/group';
  5. export enum ProblemSpan {
  6. PARENT = 'parent',
  7. OFFENDER = 'offender',
  8. CAUSE = 'cause',
  9. }
  10. export const EXAMPLE_TRANSACTION_TITLE = '/api/0/transaction-test-endpoint/';
  11. type AddSpanOpts = {
  12. endTimestamp: number;
  13. startTimestamp: number;
  14. data?: Record<string, any>;
  15. description?: string;
  16. hash?: string;
  17. op?: string;
  18. problemSpan?: ProblemSpan | ProblemSpan[];
  19. status?: string;
  20. };
  21. interface TransactionSettings {
  22. duration?: number;
  23. fcp?: number;
  24. }
  25. export class TransactionEventBuilder {
  26. TRACE_ID = '8cbbc19c0f54447ab702f00263262726';
  27. ROOT_SPAN_ID = '0000000000000000';
  28. #event: EventTransaction;
  29. #spans: RawSpanType[] = [];
  30. constructor(
  31. id?: string,
  32. title?: string,
  33. problemType?: IssueType,
  34. transactionSettings?: TransactionSettings,
  35. occurenceBasedEvent?: boolean
  36. ) {
  37. const perfEvidenceData = {
  38. causeSpanIds: [],
  39. offenderSpanIds: [],
  40. parentSpanIds: [],
  41. };
  42. this.#event = {
  43. id: id ?? 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  44. eventID: id ?? 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  45. title: title ?? EXAMPLE_TRANSACTION_TITLE,
  46. type: EventOrGroupType.TRANSACTION,
  47. startTimestamp: 0,
  48. endTimestamp: transactionSettings?.duration ?? 0,
  49. contexts: {
  50. trace: {
  51. trace_id: this.TRACE_ID,
  52. span_id: this.ROOT_SPAN_ID,
  53. op: 'pageload',
  54. status: 'ok',
  55. type: 'trace',
  56. },
  57. },
  58. entries: [
  59. {
  60. data: this.#spans,
  61. type: EntryType.SPANS,
  62. },
  63. ],
  64. // For the purpose of mock data, we don't care as much about the properties below.
  65. // They're here to satisfy the type constraints, but in the future if we need actual values here
  66. // for testing purposes, we can add methods on the builder to set them.
  67. crashFile: null,
  68. culprit: '',
  69. dateReceived: '',
  70. dist: null,
  71. errors: [],
  72. fingerprints: [],
  73. location: null,
  74. message: '',
  75. measurements: {
  76. fcp: {
  77. value: transactionSettings?.fcp ?? 0,
  78. unit: 'millisecond',
  79. },
  80. },
  81. perfProblem: undefined,
  82. metadata: {
  83. current_level: undefined,
  84. current_tree_label: undefined,
  85. directive: undefined,
  86. display_title_with_tree_label: undefined,
  87. filename: undefined,
  88. finest_tree_label: undefined,
  89. function: undefined,
  90. message: undefined,
  91. origin: undefined,
  92. stripped_crash: undefined,
  93. title: undefined,
  94. type: undefined,
  95. uri: undefined,
  96. value: undefined,
  97. },
  98. occurrence: null,
  99. projectID: '',
  100. size: 0,
  101. tags: [],
  102. user: null,
  103. };
  104. if (occurenceBasedEvent) {
  105. this.#event.occurrence = {
  106. evidenceData: perfEvidenceData,
  107. eventId: id ?? 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  108. detectionTime: '100',
  109. evidenceDisplay: [],
  110. fingerprint: ['fingerprint123'],
  111. id: 'id123',
  112. issueTitle: 'N + 1 Query',
  113. resourceId: '',
  114. subtitle: 'SELECT * FROM TABLE',
  115. type: 1006,
  116. };
  117. } else {
  118. this.#event.perfProblem = perfEvidenceData;
  119. this.#event.perfProblem.issueType =
  120. problemType ?? IssueType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES;
  121. }
  122. }
  123. generateSpanId() {
  124. // Convert the num of spans to a hex string to get its ID
  125. return (this.#spans.length + 1).toString(16).padStart(16, '0');
  126. }
  127. addEntry(entry: EventTransaction['entries'][number]) {
  128. this.#event.entries.push(entry);
  129. }
  130. addSpan(mockSpan: MockSpan, numSpans = 1, parentSpanId?: string) {
  131. for (let i = 0; i < numSpans; i++) {
  132. const spanId = this.generateSpanId();
  133. const {span} = mockSpan;
  134. const clonedSpan = {...span};
  135. clonedSpan.span_id = spanId;
  136. clonedSpan.trace_id = this.TRACE_ID;
  137. clonedSpan.parent_span_id = parentSpanId ?? this.ROOT_SPAN_ID;
  138. this.#spans.push(clonedSpan);
  139. const problemSpans = Array.isArray(mockSpan.problemSpan)
  140. ? mockSpan.problemSpan
  141. : [mockSpan.problemSpan];
  142. const perfEvidenceData =
  143. this.#event.perfProblem ?? this.#event.occurrence?.evidenceData;
  144. problemSpans.forEach(problemSpan => {
  145. switch (problemSpan) {
  146. case ProblemSpan.PARENT:
  147. perfEvidenceData?.parentSpanIds.push(spanId);
  148. break;
  149. case ProblemSpan.OFFENDER:
  150. perfEvidenceData?.offenderSpanIds.push(spanId);
  151. break;
  152. case ProblemSpan.CAUSE:
  153. perfEvidenceData?.causeSpanIds.push(spanId);
  154. break;
  155. default:
  156. break;
  157. }
  158. });
  159. if (clonedSpan.timestamp > this.#event.endTimestamp) {
  160. this.#event.endTimestamp = clonedSpan.timestamp;
  161. }
  162. mockSpan.children.forEach(child => this.addSpan(child, 1, spanId));
  163. }
  164. return this;
  165. }
  166. getEventFixture() {
  167. return this.#event;
  168. }
  169. }
  170. /**
  171. * A MockSpan object to be used for testing. This object is intended to be used in tandem with `TransactionEventBuilder`
  172. */
  173. export class MockSpan {
  174. span: RawSpanType;
  175. children: MockSpan[] = [];
  176. problemSpan: ProblemSpan | ProblemSpan[] | undefined;
  177. /**
  178. *
  179. * @param opts.startTimestamp
  180. * @param opts.endTimestamp
  181. * @param opts.op The operation of the span
  182. * @param opts.description The description of the span
  183. * @param opts.status Optional span specific status, defaults to 'ok'
  184. * @param opts.problemSpan If this span should be part of a performance problem, indicates the type of problem span (i.e ProblemSpan.OFFENDER, ProblemSpan.PARENT)
  185. * @param opts.parentSpanId When provided, will explicitly set this span's parent ID. If you are creating nested spans via `addChild` on the `MockSpan` object,
  186. * this will be handled automatically and you do not need to provide an ID. Defaults to the root span's ID.
  187. */
  188. constructor(opts: AddSpanOpts) {
  189. const {startTimestamp, endTimestamp, op, description, hash, status, problemSpan} =
  190. opts;
  191. this.span = {
  192. start_timestamp: startTimestamp,
  193. timestamp: endTimestamp,
  194. op,
  195. description,
  196. hash,
  197. status: status ?? 'ok',
  198. data: opts.data || {},
  199. // These values are automatically assigned by the TransactionEventBuilder when the spans are added
  200. span_id: '',
  201. trace_id: '',
  202. parent_span_id: '',
  203. };
  204. this.problemSpan = problemSpan;
  205. }
  206. /**
  207. *
  208. * @param opts.numSpans If provided, will create the same span numSpan times
  209. */
  210. addChild(opts: AddSpanOpts, numSpans = 1) {
  211. const {startTimestamp, endTimestamp, op, description, hash, status, problemSpan} =
  212. opts;
  213. for (let i = 0; i < numSpans; i++) {
  214. const span = new MockSpan({
  215. startTimestamp,
  216. endTimestamp,
  217. op,
  218. description,
  219. hash,
  220. status,
  221. problemSpan,
  222. });
  223. this.children.push(span);
  224. }
  225. return this;
  226. }
  227. /**
  228. * Allows you to create a nested group of duplicate mock spans by duplicating the current span. This is useful for simulating the nested 'autogrouped' condition on the span tree.
  229. * Will create `depth` spans, each span being a child of the previous.
  230. * @param depth
  231. */
  232. addDuplicateNestedChildren(depth = 1) {
  233. let currentSpan: MockSpan = this;
  234. for (let i = 0; i < depth; i++) {
  235. currentSpan.addChild(currentSpan.getOpts());
  236. currentSpan = currentSpan.children[0];
  237. }
  238. return this;
  239. }
  240. getOpts() {
  241. return {
  242. startTimestamp: this.span.start_timestamp,
  243. endTimestamp: this.span.timestamp,
  244. op: this.span.op,
  245. description: this.span.description,
  246. status: this.span.status,
  247. problemSpan: this.problemSpan,
  248. };
  249. }
  250. }