Browse Source

tests(perf-issues): Add testing utilities for events + SpanEvidence tests (#39720)

Co-authored-by: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com>
Ash Anand 2 years ago
parent
commit
3ebe13153d

+ 117 - 0
static/app/components/events/interfaces/performance/spanEvidence.spec.tsx

@@ -0,0 +1,117 @@
+import {initializeData} from 'sentry-test/performance/initializePerformanceData';
+import {
+  EXAMPLE_TRANSACTION_TITLE,
+  MockSpan,
+  ProblemSpan,
+  TransactionEventBuilder,
+} from 'sentry-test/performance/utils';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {EventTransaction} from 'sentry/types';
+import {OrganizationContext} from 'sentry/views/organizationContext';
+import {RouteContext} from 'sentry/views/routeContext';
+
+import {SpanEvidenceSection} from './spanEvidence';
+
+const {organization, router} = initializeData({
+  features: ['performance-issues'],
+});
+
+const WrappedComponent = ({event}: {event: EventTransaction}) => (
+  <OrganizationContext.Provider value={organization}>
+    <RouteContext.Provider
+      value={{
+        router,
+        location: router.location,
+        params: {},
+        routes: [],
+      }}
+    >
+      <SpanEvidenceSection organization={organization} event={event} />
+    </RouteContext.Provider>
+  </OrganizationContext.Provider>
+);
+
+describe('spanEvidence', () => {
+  it('renders and highlights the correct data in the span evidence section', () => {
+    const builder = new TransactionEventBuilder();
+    builder.addSpan(
+      new MockSpan({
+        startTimestamp: 0,
+        endTimestamp: 100,
+        op: 'http',
+        description: 'do a thing',
+      })
+    );
+
+    builder.addSpan(
+      new MockSpan({
+        startTimestamp: 100,
+        endTimestamp: 200,
+        op: 'db',
+        description: 'SELECT col FROM table',
+      })
+    );
+
+    builder.addSpan(
+      new MockSpan({
+        startTimestamp: 200,
+        endTimestamp: 300,
+        op: 'db',
+        description: 'SELECT col2 FROM table',
+      })
+    );
+
+    builder.addSpan(
+      new MockSpan({
+        startTimestamp: 200,
+        endTimestamp: 300,
+        op: 'db',
+        description: 'SELECT col3 FROM table',
+      })
+    );
+
+    const parentProblemSpan = new MockSpan({
+      startTimestamp: 300,
+      endTimestamp: 600,
+      op: 'db',
+      description: 'connect',
+      problemSpan: ProblemSpan.PARENT,
+    });
+    parentProblemSpan.addChild(
+      {
+        startTimestamp: 300,
+        endTimestamp: 600,
+        op: 'db',
+        description: 'group me',
+        problemSpan: ProblemSpan.OFFENDER,
+      },
+      9
+    );
+
+    builder.addSpan(parentProblemSpan);
+
+    render(<WrappedComponent event={builder.getEvent()} />);
+
+    // Verify the surfaced fields in the span evidence section are correct
+    const transactionKey = screen.getByRole('cell', {name: 'Transaction'});
+    const transactionVal = screen.getByRole('cell', {name: EXAMPLE_TRANSACTION_TITLE});
+    expect(transactionKey).toBeInTheDocument();
+    expect(transactionVal).toBeInTheDocument();
+
+    const parentKey = screen.getByRole('cell', {name: 'Parent Span'});
+    const parentVal = screen.getByRole('cell', {name: 'db - connect'});
+    expect(parentKey).toBeInTheDocument();
+    expect(parentVal).toBeInTheDocument();
+
+    const repeatingKey = screen.getByRole('cell', {name: 'Repeating Span'});
+    const repeatingVal = screen.getByRole('cell', {name: 'db - group me'});
+    expect(repeatingKey).toBeInTheDocument();
+    expect(repeatingVal).toBeInTheDocument();
+
+    // Verify that the correct spans are hi-lighted on the span tree as affected spans
+    const affectedSpan = screen.getByTestId('row-title-content-affected');
+    expect(affectedSpan).toBeInTheDocument();
+    expect(affectedSpan).toHaveTextContent('db — connect');
+  });
+});

+ 5 - 2
static/app/components/events/interfaces/spans/spanBar.tsx

@@ -475,7 +475,7 @@ class SpanBar extends Component<SpanBarProps, SpanBarState> {
   }
 
   renderTitle(errors: TraceError[] | null) {
-    const {generateContentSpanBarRef} = this.props;
+    const {generateContentSpanBarRef, spanBarHatch} = this.props;
     const {
       span,
       treeDepth,
@@ -539,7 +539,10 @@ class SpanBar extends Component<SpanBarProps, SpanBarState> {
             width: '100%',
           }}
         >
-          <RowTitleContent errored={errored}>
+          <RowTitleContent
+            errored={errored}
+            data-test-id={`row-title-content${spanBarHatch ? `-${spanBarHatch}` : ''}`}
+          >
             <strong>{titleFragments}</strong>
             {description}
           </RowTitleContent>

+ 5 - 56
static/app/components/events/interfaces/spans/traceView.spec.tsx

@@ -1,4 +1,8 @@
-import {initializeData as _initializeData} from 'sentry-test/performance/initializePerformanceData';
+import {
+  generateSampleEvent,
+  generateSampleSpan,
+  initializeData as _initializeData,
+} from 'sentry-test/performance/initializePerformanceData';
 import {
   act,
   render,
@@ -13,7 +17,6 @@ import TraceView from 'sentry/components/events/interfaces/spans/traceView';
 import {spanTargetHash} from 'sentry/components/events/interfaces/spans/utils';
 import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
 import ProjectsStore from 'sentry/stores/projectsStore';
-import {EntryType, EventTransaction} from 'sentry/types';
 import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext';
 import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
 import {OrganizationContext} from 'sentry/views/organizationContext';
@@ -24,60 +27,6 @@ function initializeData(settings) {
   return data;
 }
 
-function generateSampleEvent(): EventTransaction {
-  const event = {
-    id: '2b658a829a21496b87fd1f14a61abf65',
-    eventID: '2b658a829a21496b87fd1f14a61abf65',
-    title: '/organizations/:orgId/discover/results/',
-    type: 'transaction',
-    startTimestamp: 1622079935.86141,
-    endTimestamp: 1622079940.032905,
-    contexts: {
-      trace: {
-        trace_id: '8cbbc19c0f54447ab702f00263262726',
-        span_id: 'a000000000000000',
-        op: 'pageload',
-        status: 'unknown',
-        type: 'trace',
-      },
-    },
-    entries: [
-      {
-        data: [],
-        type: EntryType.SPANS,
-      },
-    ],
-  } as EventTransaction;
-
-  return event;
-}
-
-function generateSampleSpan(
-  description: string | null,
-  op: string | null,
-  span_id: string,
-  parent_span_id: string,
-  event: EventTransaction
-) {
-  const span = {
-    start_timestamp: 1000,
-    timestamp: 2000,
-    description,
-    op,
-    span_id,
-    parent_span_id,
-    trace_id: '8cbbc19c0f54447ab702f00263262726',
-    status: 'ok',
-    tags: {
-      'http.status_code': '200',
-    },
-    data: {},
-  };
-
-  event.entries[0].data.push(span);
-  return span;
-}
-
 const WrappedTraceView = ({organization, waterfallModel}) => (
   <OrganizationContext.Provider value={organization}>
     <TraceView organization={organization} waterfallModel={waterfallModel} />

+ 55 - 1
tests/js/sentry-test/performance/initializePerformanceData.ts

@@ -1,6 +1,6 @@
 import {initializeOrg} from 'sentry-test/initializeOrg';
 
-import {Project} from 'sentry/types';
+import {EntryType, EventTransaction, Project} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import EventView from 'sentry/utils/discover/eventView';
 import {
@@ -175,3 +175,57 @@ export function generateSuspectSpansResponse(opts?: {
     return suspectSpans;
   });
 }
+
+export function generateSampleEvent(): EventTransaction {
+  const event = {
+    id: '2b658a829a21496b87fd1f14a61abf65',
+    eventID: '2b658a829a21496b87fd1f14a61abf65',
+    title: '/organizations/:orgId/discover/results/',
+    type: 'transaction',
+    startTimestamp: 1622079935.86141,
+    endTimestamp: 1622079940.032905,
+    contexts: {
+      trace: {
+        trace_id: '8cbbc19c0f54447ab702f00263262726',
+        span_id: 'a000000000000000',
+        op: 'pageload',
+        status: 'unknown',
+        type: 'trace',
+      },
+    },
+    entries: [
+      {
+        data: [],
+        type: EntryType.SPANS,
+      },
+    ],
+  } as EventTransaction;
+
+  return event;
+}
+
+export function generateSampleSpan(
+  description: string | null,
+  op: string | null,
+  span_id: string,
+  parent_span_id: string,
+  event: EventTransaction
+) {
+  const span = {
+    start_timestamp: 1000,
+    timestamp: 2000,
+    description,
+    op,
+    span_id,
+    parent_span_id,
+    trace_id: '8cbbc19c0f54447ab702f00263262726',
+    status: 'ok',
+    tags: {
+      'http.status_code': '200',
+    },
+    data: {},
+  };
+
+  event.entries[0].data.push(span);
+  return span;
+}

+ 182 - 0
tests/js/sentry-test/performance/utils.ts

@@ -0,0 +1,182 @@
+import {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
+import {EntryType, EventOrGroupType, EventTransaction} from 'sentry/types';
+
+export enum ProblemSpan {
+  PARENT = 'parent',
+  OFFENDER = 'offender',
+}
+
+export const EXAMPLE_TRANSACTION_TITLE = '/api/0/transaction-test-endpoint/';
+
+type AddSpanOpts = {
+  endTimestamp: number;
+  startTimestamp: number;
+  childOpts?: AddSpanOpts[];
+  description?: string;
+  op?: string;
+  problemSpan?: ProblemSpan;
+  status?: string;
+};
+
+export class TransactionEventBuilder {
+  TRACE_ID = '8cbbc19c0f54447ab702f00263262726';
+  ROOT_SPAN_ID = '0000000000000000';
+  #event: EventTransaction;
+  #spans: RawSpanType[] = [];
+
+  constructor(id?: string, title?: string) {
+    this.#event = {
+      id: id ?? 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+      eventID: id ?? 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+      title: title ?? EXAMPLE_TRANSACTION_TITLE,
+      type: EventOrGroupType.TRANSACTION,
+      startTimestamp: 0,
+      endTimestamp: 0,
+      contexts: {
+        trace: {
+          trace_id: this.TRACE_ID,
+          span_id: this.ROOT_SPAN_ID,
+          op: 'pageload',
+          status: 'ok',
+          type: 'trace',
+        },
+      },
+      entries: [
+        {
+          data: this.#spans,
+          type: EntryType.SPANS,
+        },
+      ],
+      perfProblem: {
+        causeSpanIds: [],
+        offenderSpanIds: [],
+        parentSpanIds: [],
+      },
+      // For the purpose of mock data, we don't care as much about the properties below.
+      // They're here to satisfy the type constraints, but in the future if we need actual values here
+      // for testing purposes, we can add methods on the builder to set them.
+      crashFile: null,
+      culprit: '',
+      dateReceived: '',
+      dist: null,
+      errors: [],
+      fingerprints: [],
+      location: null,
+      message: '',
+      metadata: {
+        current_level: undefined,
+        current_tree_label: undefined,
+        directive: undefined,
+        display_title_with_tree_label: undefined,
+        filename: undefined,
+        finest_tree_label: undefined,
+        function: undefined,
+        message: undefined,
+        origin: undefined,
+        stripped_crash: undefined,
+        title: undefined,
+        type: undefined,
+        uri: undefined,
+        value: undefined,
+      },
+      projectID: '',
+      size: 0,
+      tags: [],
+      user: null,
+    };
+  }
+
+  addSpan(mSpan: MockSpan, parentSpanId?: string) {
+    // Convert the num of spans to a hex string to get its ID
+    const spanId = (this.#spans.length + 1).toString(16).padStart(16, '0');
+    const {span} = mSpan;
+    span.span_id = spanId;
+    span.trace_id = this.TRACE_ID;
+    span.parent_span_id = parentSpanId ?? this.ROOT_SPAN_ID;
+
+    this.#event.entries[0].data.push(span);
+
+    switch (mSpan.problemSpan) {
+      case ProblemSpan.PARENT:
+        this.#event.perfProblem?.parentSpanIds.push(spanId);
+        break;
+      case ProblemSpan.OFFENDER:
+        this.#event.perfProblem?.offenderSpanIds.push(spanId);
+        break;
+      default:
+        break;
+    }
+
+    if (span.timestamp > this.#event.endTimestamp) {
+      this.#event.endTimestamp = span.timestamp;
+    }
+
+    mSpan.children.forEach(child => this.addSpan(child, spanId));
+  }
+
+  getEvent() {
+    return this.#event;
+  }
+}
+
+/**
+ * A MockSpan object to be used for testing. This object is intended to be used in tandem with `TransactionEventBuilder`
+ */
+export class MockSpan {
+  span: RawSpanType;
+  children: MockSpan[] = [];
+  problemSpan: ProblemSpan | undefined;
+
+  /**
+   *
+   * @param opts.startTimestamp
+   * @param opts.endTimestamp
+   * @param opts.op The operation of the span
+   * @param opts.description The description of the span
+   * @param opts.status Optional span specific status, defaults to 'ok'
+   * @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)
+   * @param opts.parentSpanId When provided, will explicitly set this span's parent ID. If you are creating nested spans via `childOpts`, this will be handled automatically and you do not need to provide an ID.
+   * Defaults to the root span's ID.
+   * @param opts.childOpts An array containing options for direct children of the current span. Will create direct child spans for each set of options provided
+   */
+  constructor(opts: AddSpanOpts) {
+    const {startTimestamp, endTimestamp, op, description, status, problemSpan} = opts;
+
+    this.span = {
+      start_timestamp: startTimestamp,
+      timestamp: endTimestamp,
+      op,
+      description,
+      status: status ?? 'ok',
+      data: {},
+      // These values are automatically assigned by the TransactionEventBuilder when the spans are added
+      span_id: '',
+      trace_id: '',
+      parent_span_id: '',
+    };
+
+    this.problemSpan = problemSpan;
+  }
+
+  /**
+   *
+   * @param opts.numSpans If provided, will create the same span numSpan times
+   */
+  addChild(opts: AddSpanOpts, numSpans = 1) {
+    const {startTimestamp, endTimestamp, op, description, status, problemSpan} = opts;
+
+    for (let i = 0; i < numSpans; i++) {
+      const span = new MockSpan({
+        startTimestamp,
+        endTimestamp,
+        op,
+        description,
+        status,
+        problemSpan,
+      });
+      this.children.push(span);
+    }
+
+    return this;
+  }
+}