From 441cf2d9e8f348d998862485ffa16b005919d3ae Mon Sep 17 00:00:00 2001
From: Daniele Rossetti <drossetti@gitlab.com>
Date: Fri, 22 Mar 2024 18:49:34 +0000
Subject: [PATCH] Improve support for incomplete traces

---
 .../tracing/details/tracing_header.vue        | 23 +++++++++--
 .../tracing/list/tracing_table.vue            | 10 ++++-
 .../assets/javascripts/tracing/trace_utils.js |  4 +-
 .../tracing/details/tracing_header_spec.js    | 38 +++++++++++++++----
 .../tracing/list/tracing_table_spec.js        | 34 +++++++++++++++--
 ee/spec/frontend/tracing/trace_utils_spec.js  | 37 ++++++++++++++++++
 locale/gitlab.pot                             |  3 ++
 7 files changed, 133 insertions(+), 16 deletions(-)

diff --git a/ee/app/assets/javascripts/tracing/details/tracing_header.vue b/ee/app/assets/javascripts/tracing/details/tracing_header.vue
index 4a442637bb8ea..bb03c1924ba3a 100644
--- a/ee/app/assets/javascripts/tracing/details/tracing_header.vue
+++ b/ee/app/assets/javascripts/tracing/details/tracing_header.vue
@@ -1,7 +1,8 @@
 <script>
-import { GlCard } from '@gitlab/ui';
+import { GlCard, GlBadge } from '@gitlab/ui';
 import { formatDate } from '~/lib/utils/datetime/date_format_utility';
-import { formatTraceDuration } from '../trace_utils';
+import { s__ } from '~/locale';
+import { formatTraceDuration, findRootSpan } from '../trace_utils';
 
 const CARD_CLASS = 'gl-mr-7 gl-w-15p gl-min-w-fit-content';
 const HEADER_CLASS = 'gl-p-2 gl-font-weight-bold gl--flex-center';
@@ -14,6 +15,10 @@ export default {
   BODY_CLASS,
   components: {
     GlCard,
+    GlBadge,
+  },
+  i18n: {
+    inProgress: s__('Tracing|In progress'),
   },
   props: {
     trace: {
@@ -34,13 +39,25 @@ export default {
     traceDuration() {
       return formatTraceDuration(this.trace.duration_nano);
     },
+    isTraceInProgress() {
+      return !findRootSpan(this.trace);
+    },
   },
 };
 </script>
 
 <template>
   <div class="gl-mb-6">
-    <h1>{{ title }}</h1>
+    <h1>
+      {{ title }}
+      <gl-badge
+        v-if="isTraceInProgress"
+        variant="warning"
+        size="md"
+        class="gl-ml-3 gl-vertical-align-middle"
+        >{{ $options.i18n.inProgress }}</gl-badge
+      >
+    </h1>
 
     <div class="gl-display-flex gl-flex-wrap gl-justify-content-center gl-my-7 gl-row-gap-6">
       <gl-card
diff --git a/ee/app/assets/javascripts/tracing/list/tracing_table.vue b/ee/app/assets/javascripts/tracing/list/tracing_table.vue
index 3b0045e229bbe..bc72ee6d00a24 100644
--- a/ee/app/assets/javascripts/tracing/list/tracing_table.vue
+++ b/ee/app/assets/javascripts/tracing/list/tracing_table.vue
@@ -9,6 +9,7 @@ export default {
   i18n: {
     title: s__('Tracing|Traces'),
     emptyText: __('No results found'),
+    inProgress: s__('Tracing|In progress'),
   },
   fields: [
     {
@@ -71,7 +72,11 @@ export default {
     },
     matchesBadgeContent(item) {
       const spans = n__('Tracing|%d span', 'Tracing|%d spans', item.total_spans);
-      if (item.total_spans === item.matched_span_count) {
+      if (
+        item.total_spans === item.matched_span_count ||
+        !Number.isInteger(item.matched_span_count) ||
+        item.in_progress
+      ) {
         return spans;
       }
       const matches = n__('Tracing|%d match', 'Tracing|%d matches', item.matched_span_count);
@@ -108,6 +113,9 @@ export default {
         {{ item.timestamp }}
         <div class="gl-mt-4 gl-display-flex">
           <gl-badge variant="info" size="md">{{ matchesBadgeContent(item) }}</gl-badge>
+          <gl-badge v-if="item.in_progress" variant="warning" size="md" class="gl-ml-3">{{
+            $options.i18n.inProgress
+          }}</gl-badge>
           <gl-badge v-if="hasError(item)" variant="danger" size="md" class="gl-ml-2">
             <gl-icon name="status-alert" class="gl-mr-2 gl-text-red-500" />
             {{ errorBadgeContent(item) }}
diff --git a/ee/app/assets/javascripts/tracing/trace_utils.js b/ee/app/assets/javascripts/tracing/trace_utils.js
index cd7a8c579a5f2..38fa19a9c9e5f 100644
--- a/ee/app/assets/javascripts/tracing/trace_utils.js
+++ b/ee/app/assets/javascripts/tracing/trace_utils.js
@@ -69,10 +69,12 @@ export function assignColorToServices(trace) {
 
 const timestampToMs = (ts) => new Date(ts).getTime();
 
+export const findRootSpan = (trace) => trace.spans.find((s) => s.parent_span_id === '');
+
 export function mapTraceToTreeRoot(trace) {
   const nodes = {};
 
-  const rootSpan = trace.spans.find((s) => s.parent_span_id === '');
+  const rootSpan = findRootSpan(trace);
   if (!rootSpan) return undefined;
 
   const spanToNode = (span) => ({
diff --git a/ee/spec/frontend/tracing/details/tracing_header_spec.js b/ee/spec/frontend/tracing/details/tracing_header_spec.js
index a0607a49f77ef..570b9009a0dad 100644
--- a/ee/spec/frontend/tracing/details/tracing_header_spec.js
+++ b/ee/spec/frontend/tracing/details/tracing_header_spec.js
@@ -4,24 +4,46 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 describe('TracingHeader', () => {
   let wrapper;
 
-  beforeEach(() => {
+  const defaultTrace = {
+    service_name: 'Service',
+    operation: 'Operation',
+    timestamp: 1692021937219,
+    duration_nano: 1000000000,
+    total_spans: 10,
+    spans: [
+      { span_id: 'span-1', parent_span_id: '' },
+      { span_id: 'span-2', parent_span_id: 'span-1' },
+    ],
+  };
+
+  const createComponent = (trace = defaultTrace) => {
     wrapper = shallowMountExtended(TracingHeader, {
       propsData: {
-        trace: {
-          service_name: 'Service',
-          operation: 'Operation',
-          timestamp: 1692021937219,
-          duration_nano: 1000000000,
-          total_spans: 10,
-        },
+        trace,
       },
     });
+  };
+  beforeEach(() => {
+    createComponent();
   });
 
   it('renders the correct title', () => {
     expect(wrapper.find('h1').text()).toBe('Service : Operation');
   });
 
+  it('does not show the in progress label when the root span is not missing', () => {
+    expect(wrapper.find('h1').text()).not.toContain('In progress');
+  });
+
+  it('shows the in progress label when the root span is missing', () => {
+    createComponent({
+      ...defaultTrace,
+      spans: [{ span_id: 'span-2', parent_span_id: 'span-1' }],
+    });
+
+    expect(wrapper.find('h1').text()).toContain('In progress');
+  });
+
   it('renders the correct trace date', () => {
     expect(wrapper.findByTestId('trace-date-card').text()).toMatchInterpolatedText(
       'Trace start Aug 14, 2023 14:05:37.219 UTC',
diff --git a/ee/spec/frontend/tracing/list/tracing_table_spec.js b/ee/spec/frontend/tracing/list/tracing_table_spec.js
index 733536cf74876..f089dd04ba473 100644
--- a/ee/spec/frontend/tracing/list/tracing_table_spec.js
+++ b/ee/spec/frontend/tracing/list/tracing_table_spec.js
@@ -25,6 +25,17 @@ describe('TracingTable', () => {
       matched_span_count: 2,
       error_span_count: 1,
     },
+    {
+      timestamp: '2023-08-11T16:03:50.577538Z',
+      service_name: 'tracegen-3',
+      operation: 'lets-go-3',
+      duration_nano: 2000000,
+      trace_id: 'trace-3',
+      total_spans: 3,
+      matched_span_count: 2,
+      error_span_count: 1,
+      in_progress: true,
+    },
   ];
 
   const expectedTraces = [
@@ -46,6 +57,16 @@ describe('TracingTable', () => {
       duration: '2ms',
       trace_id: 'trace-2',
     },
+    {
+      timestamp: 'Aug 11, 2023 4:03pm UTC',
+      badge: '3 spans',
+      errorBadge: '1 error',
+      service_name: 'tracegen-3',
+      operation: 'lets-go-3',
+      duration: '2ms',
+      trace_id: 'trace-3',
+      inProgressBadge: true,
+    },
   ];
 
   const mountComponent = ({ traces = mockTraces, highlightedTraceId } = {}) => {
@@ -74,15 +95,22 @@ describe('TracingTable', () => {
       const row = getRows().at(i);
       const expected = expectedTraces[i];
       expect(row.find(`[data-testid="trace-timestamp"]`).text()).toContain(expected.timestamp);
+      expect(row.find(`[data-testid="trace-service"]`).text()).toBe(expected.service_name);
+      expect(row.find(`[data-testid="trace-operation"]`).text()).toBe(expected.operation);
+      expect(row.find(`[data-testid="trace-duration"]`).text()).toBe(expected.duration);
       expect(row.find(`[data-testid="trace-timestamp"]`).text()).toContain(expected.badge);
+
       if (expected.errorBadge) {
         expect(row.find(`[data-testid="trace-timestamp"]`).text()).toContain(expected.errorBadge);
       } else {
         expect(row.find(`[data-testid="trace-timestamp"]`).text()).not.toContain('error');
       }
-      expect(row.find(`[data-testid="trace-service"]`).text()).toBe(expected.service_name);
-      expect(row.find(`[data-testid="trace-operation"]`).text()).toBe(expected.operation);
-      expect(row.find(`[data-testid="trace-duration"]`).text()).toBe(expected.duration);
+
+      if (expected.inProgressBadge) {
+        expect(row.find(`[data-testid="trace-timestamp"]`).text()).toContain('In progress');
+      } else {
+        expect(row.find(`[data-testid="trace-timestamp"]`).text()).not.toContain('In progress');
+      }
     });
   });
 
diff --git a/ee/spec/frontend/tracing/trace_utils_spec.js b/ee/spec/frontend/tracing/trace_utils_spec.js
index 00ae7683de973..df7e4873f8849 100644
--- a/ee/spec/frontend/tracing/trace_utils_spec.js
+++ b/ee/spec/frontend/tracing/trace_utils_spec.js
@@ -6,6 +6,7 @@ import {
   formatTraceDuration,
   assignColorToServices,
   periodFilterToDate,
+  findRootSpan,
 } from 'ee/tracing/trace_utils';
 
 describe('trace_utils', () => {
@@ -96,6 +97,42 @@ describe('trace_utils', () => {
     });
   });
 
+  describe('findRootSpan', () => {
+    const rootSpan = {
+      timestamp: '2023-08-07T15:03:53.199871Z',
+      span_id: 'SPAN-1',
+      trace_id: 'TRACE-1',
+      service_name: 'SERVICE-1',
+      operation: 'OP-1',
+      duration_nano: 123456789,
+      parent_span_id: '',
+    };
+    const nonRootSpan = {
+      timestamp: '2023-08-07T15:03:53.199871Z',
+      span_id: 'SPAN-2',
+      trace_id: 'TRACE-2',
+      service_name: 'SERVICE-2',
+      operation: 'OP-2',
+      duration_nano: 123456789,
+      parent_span_id: 'SPAN-1',
+    };
+    it('returns the root span', () => {
+      expect(
+        findRootSpan({
+          spans: [nonRootSpan, rootSpan],
+        }),
+      ).toBe(rootSpan);
+    });
+
+    it('returns undefined if the root span is missing', () => {
+      expect(
+        findRootSpan({
+          spans: [nonRootSpan],
+        }),
+      ).toBeUndefined();
+    });
+  });
+
   describe('mapTraceToTreeRoot', () => {
     it('should map a trace data to tree data and return the root node', () => {
       const trace = {
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 352ba946d2b05..5182c7321edf6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -53281,6 +53281,9 @@ msgstr ""
 msgid "Tracing|Filter traces"
 msgstr ""
 
+msgid "Tracing|In progress"
+msgstr ""
+
 msgid "Tracing|Metadata"
 msgstr ""
 
-- 
GitLab