Browse Source

ref(event-tags): Refactor work for Issue Details changes (#69426)

No significant changes here, but a lot of the util/refactors/new props
are necessary for enabling rendering custom highlights.

List of changes:
- Pulls out the list of context items from the `EventContexts` component
- Pulls the context content out of the `ContextCard` component (with a
config to edit how its rendered)
- Adds a config to `EventTagsTreeRow` component to edit how its rendered
- Remove a div from the `EventTagsTree`
- Creates the `ContextCard` components from the `EventContexts`
component into the `ContextDataSection`
- Implement a hook to get a detailed project from a slug (since
`useProjects` does not render with the `DetailedProjectSerializer`)
- Correct the flag name on some frontend tests
- Rename param for `useIssueDetailsColumnCount` hook
- Adds `highlightContext` and `highlightTags` to the Project type
Leander Rodrigues 10 months ago
parent
commit
496d5a6e63

+ 73 - 47
static/app/components/events/contexts/contextCard.tsx

@@ -1,6 +1,8 @@
 import {Link} from 'react-router';
 import styled from '@emotion/styled';
+import startCase from 'lodash/startCase';
 
+import type {ContextValue} from 'sentry/components/events/contexts';
 import {
   getContextMeta,
   getContextTitle,
@@ -10,9 +12,7 @@ import {AnnotatedTextErrors} from 'sentry/components/events/meta/annotatedText/a
 import Panel from 'sentry/components/panels/panel';
 import {StructuredData} from 'sentry/components/structuredEventData';
 import {space} from 'sentry/styles/space';
-import type {Event} from 'sentry/types/event';
-import type {Group} from 'sentry/types/group';
-import type {Project} from 'sentry/types/project';
+import type {Event, Group, KeyValueListDataItem, Project} from 'sentry/types';
 import {defined, objectIsEmpty} from 'sentry/utils';
 import useOrganization from 'sentry/utils/useOrganization';
 
@@ -22,10 +22,74 @@ interface ContextCardProps {
   type: string;
   group?: Group;
   project?: Project;
-  value?: Record<string, any>;
+  value?: ContextValue;
 }
 
-function ContextCard({alias, event, type, project, value = {}}: ContextCardProps) {
+interface ContextCardContentConfig {
+  disableErrors?: boolean;
+  includeAliasInSubject?: boolean;
+}
+
+export interface ContextCardContentProps {
+  item: KeyValueListDataItem;
+  meta: Record<string, any>;
+  alias?: string;
+  config?: ContextCardContentConfig;
+}
+
+export function ContextCardContent({
+  item,
+  alias,
+  meta,
+  config,
+  ...props
+}: ContextCardContentProps) {
+  const {key: contextKey, subject, value: contextValue, action = {}} = item;
+  if (contextKey === 'type') {
+    return null;
+  }
+  const contextMeta = meta?.[contextKey];
+  const contextErrors = contextMeta?.['']?.err ?? [];
+  const hasErrors = contextErrors.length > 0 && !config?.disableErrors;
+
+  const dataComponent = (
+    <StructuredData
+      value={contextValue}
+      depth={0}
+      maxDefaultDepth={0}
+      meta={contextMeta}
+      withAnnotatedText
+      withOnlyFormattedText
+    />
+  );
+
+  const contextSubject =
+    config?.includeAliasInSubject && alias ? `${startCase(alias)}: ${subject}` : subject;
+
+  return (
+    <ContextContent hasErrors={hasErrors} {...props}>
+      <ContextSubject>{contextSubject}</ContextSubject>
+      <ContextValueWrapper hasErrors={hasErrors} className="ctx-row-value">
+        {defined(action?.link) ? (
+          <Link to={action.link}>{dataComponent}</Link>
+        ) : (
+          dataComponent
+        )}
+      </ContextValueWrapper>
+      <ContextErrors>
+        <AnnotatedTextErrors errors={contextErrors} />
+      </ContextErrors>
+    </ContextContent>
+  );
+}
+
+export default function ContextCard({
+  alias,
+  event,
+  type,
+  project,
+  value = {},
+}: ContextCardProps) {
   const organization = useOrganization();
   if (objectIsEmpty(value)) {
     return null;
@@ -40,43 +104,9 @@ function ContextCard({alias, event, type, project, value = {}}: ContextCardProps
     project,
   });
 
-  const content = contextItems.map(
-    ({key, subject, value: contextValue, action = {}}, i) => {
-      if (key === 'type') {
-        return null;
-      }
-      const contextMeta = meta?.[key];
-      const contextErrors = contextMeta?.['']?.err ?? [];
-      const hasErrors = contextErrors.length > 0;
-
-      const dataComponent = (
-        <StructuredData
-          value={contextValue}
-          depth={0}
-          maxDefaultDepth={0}
-          meta={contextMeta}
-          withAnnotatedText
-          withOnlyFormattedText
-        />
-      );
-
-      return (
-        <ContextContent key={i} hasErrors={hasErrors}>
-          <ContextSubject>{subject}</ContextSubject>
-          <ContextValue hasErrors={hasErrors}>
-            {defined(action?.link) ? (
-              <Link to={action.link}>{dataComponent}</Link>
-            ) : (
-              dataComponent
-            )}
-          </ContextValue>
-          <ContextErrors>
-            <AnnotatedTextErrors errors={contextErrors} />
-          </ContextErrors>
-        </ContextContent>
-      );
-    }
-  );
+  const content = contextItems.map((item, i) => (
+    <ContextCardContent key={`context-card-${i}`} meta={meta} item={item} />
+  ));
 
   return (
     <Card>
@@ -125,13 +155,9 @@ const ContextSubject = styled('div')`
   word-wrap: break-word;
 `;
 
-const ContextValue = styled(ContextSubject)<{hasErrors: boolean}>`
+const ContextValueWrapper = styled(ContextSubject)<{hasErrors: boolean}>`
   color: ${p => (p.hasErrors ? 'inherit' : p.theme.textColor)};
   grid-column: span ${p => (p.hasErrors ? 1 : 2)};
-  /* justify-content: space-between;
-  display: inline-flex; */
 `;
 
 const ContextErrors = styled('div')``;
-
-export default ContextCard;

+ 20 - 2
static/app/components/events/contexts/contextDataSection.tsx

@@ -1,20 +1,38 @@
 import {useRef} from 'react';
 import styled from '@emotion/styled';
 
+import {getOrderedContextItems} from 'sentry/components/events/contexts';
+import ContextCard from 'sentry/components/events/contexts/contextCard';
 import {CONTEXT_DOCS_LINK} from 'sentry/components/events/contextSummary/utils';
 import {EventDataSection} from 'sentry/components/events/eventDataSection';
 import {useIssueDetailsColumnCount} from 'sentry/components/events/eventTags/util';
 import ExternalLink from 'sentry/components/links/externalLink';
 import {t, tct} from 'sentry/locale';
+import type {Event, Group, Project} from 'sentry/types';
 
 interface ContextDataSectionProps {
-  cards: React.ReactNode[];
+  event: Event;
+  group?: Group;
+  project?: Project;
 }
 
-function ContextDataSection({cards}: ContextDataSectionProps) {
+function ContextDataSection({event, group, project}: ContextDataSectionProps) {
   const containerRef = useRef<HTMLDivElement>(null);
   const columnCount = useIssueDetailsColumnCount(containerRef);
   const columns: React.ReactNode[] = [];
+
+  const cards = getOrderedContextItems(event).map(({alias, value: contextValue}) => (
+    <ContextCard
+      key={alias}
+      type={contextValue.type}
+      alias={alias}
+      value={contextValue}
+      event={event}
+      group={group}
+      project={project}
+    />
+  ));
+
   const columnSize = Math.ceil(cards.length / columnCount);
   for (let i = 0; i < cards.length; i += columnSize) {
     columns.push(<CardColumn key={i}>{cards.slice(i, i + columnSize)}</CardColumn>);

+ 51 - 33
static/app/components/events/contexts/index.tsx

@@ -1,10 +1,9 @@
 import {Fragment, useCallback, useEffect} from 'react';
 import * as Sentry from '@sentry/react';
 
-import ContextCard from 'sentry/components/events/contexts/contextCard';
 import ContextDataSection from 'sentry/components/events/contexts/contextDataSection';
 import {useHasNewTagsUI} from 'sentry/components/events/eventTags/util';
-import type {Event} from 'sentry/types/event';
+import type {Event, EventContexts as EventContextValues} from 'sentry/types/event';
 import type {Group} from 'sentry/types/group';
 import {objectIsEmpty} from 'sentry/utils';
 import useProjects from 'sentry/utils/useProjects';
@@ -16,6 +15,55 @@ type Props = {
   group?: Group;
 };
 
+interface UnknownContextValue {
+  [key: string]: any;
+  type: 'default';
+}
+
+/**
+ * Catch-all for context values, known and unknown
+ */
+export type ContextValue =
+  | EventContextValues[keyof EventContextValues]
+  | UnknownContextValue;
+
+export interface ContextItem {
+  alias: string;
+  type: string;
+  value: ContextValue;
+}
+
+export function getOrderedContextItems(event): ContextItem[] {
+  const {user, contexts} = event;
+
+  const {feedback, response, ...otherContexts} = contexts ?? {};
+  const orderedContext: [ContextItem['alias'], ContextValue][] = [
+    ['response', response],
+    ['feedback', feedback],
+    ['user', user],
+    ...Object.entries(otherContexts),
+  ];
+  // For these context keys, use 'key' as 'type' rather than 'value.type'
+  const overrideTypes = new Set(['response', 'feedback', 'user']);
+  const items = orderedContext
+    .filter(([_k, ctxValue]) => {
+      const contextKeys = Object.keys(ctxValue ?? {});
+      const isInvalid =
+        // Empty context
+        contextKeys.length === 0 ||
+        // Empty aside from 'type' key
+        (contextKeys.length === 1 && contextKeys[0] === 'type');
+      return !isInvalid;
+    })
+    .map<ContextItem>(([alias, ctx]) => ({
+      alias,
+      type: overrideTypes.has(ctx.type) ? ctx : ctx?.type,
+      value: ctx,
+    }));
+
+  return items;
+}
+
 export function EventContexts({event, group}: Props) {
   const hasNewTagsUI = useHasNewTagsUI();
   const {projects} = useProjects();
@@ -39,37 +87,7 @@ export function EventContexts({event, group}: Props) {
   }, [usingOtel, sdk]);
 
   if (hasNewTagsUI) {
-    const orderedContext: [string, any][] = [
-      ['response', response],
-      ['feedback', feedback],
-      ['user', user],
-      ...Object.entries(otherContexts),
-    ];
-    // For these context keys, use 'key' as 'type' rather than 'value.type'
-    const overrideTypes = new Set(['response', 'feedback', 'user']);
-    const cards = orderedContext
-      .filter(([_k, v]) => {
-        const contextKeys = Object.keys(v ?? {});
-        const isInvalid =
-          // Empty context
-          contextKeys.length === 0 ||
-          // Empty aside from 'type' key
-          (contextKeys.length === 1 && contextKeys[0] === 'type');
-        return !isInvalid;
-      })
-      .map(([k, v]) => (
-        <ContextCard
-          key={k}
-          type={overrideTypes.has(k) ? k : v?.type ?? ''}
-          alias={k}
-          value={v}
-          event={event}
-          group={group}
-          project={project}
-        />
-      ));
-
-    return <ContextDataSection cards={cards} />;
+    return <ContextDataSection event={event} group={group} project={project} />;
   }
 
   return (

+ 4 - 9
static/app/components/events/eventTags/eventTagsTree.tsx

@@ -174,25 +174,20 @@ function EventTagsTree(props: EventTagsTreeProps) {
   const containerRef = useRef<HTMLDivElement>(null);
   const columnCount = useIssueDetailsColumnCount(containerRef);
   return (
-    <TreeContainer ref={containerRef}>
-      <TreeGarden columnCount={columnCount}>
-        <TagTreeColumns columnCount={columnCount} {...props} />
-      </TreeGarden>
+    <TreeContainer columnCount={columnCount} ref={containerRef}>
+      <TagTreeColumns columnCount={columnCount} {...props} />
     </TreeContainer>
   );
 }
 
-const TreeContainer = styled('div')`
+export const TreeContainer = styled('div')<{columnCount: number}>`
   margin-top: ${space(1.5)};
-`;
-
-const TreeGarden = styled('div')<{columnCount: number}>`
   display: grid;
   grid-template-columns: repeat(${p => p.columnCount}, 1fr);
   align-items: start;
 `;
 
-const TreeColumn = styled('div')`
+export const TreeColumn = styled('div')`
   display: grid;
   grid-template-columns: minmax(auto, 175px) 1fr;
   grid-column-gap: ${space(3)};

+ 40 - 27
static/app/components/events/eventTags/eventTagsTreeRow.tsx

@@ -18,11 +18,17 @@ import {generateQueryWithTag, isUrl} from 'sentry/utils';
 import useOrganization from 'sentry/utils/useOrganization';
 import useRouter from 'sentry/utils/useRouter';
 
+interface EventTagTreeRowConfig {
+  disableActions?: boolean;
+  disableRichValue?: boolean;
+}
+
 export interface EventTagsTreeRowProps {
   content: TagTreeContent;
   event: Event;
   projectSlug: string;
   tagKey: string;
+  config?: EventTagTreeRowConfig;
   isLast?: boolean;
   spacerCount?: number;
 }
@@ -34,12 +40,14 @@ export default function EventTagsTreeRow({
   projectSlug,
   spacerCount = 0,
   isLast = false,
+  config = {},
+  ...props
 }: EventTagsTreeRowProps) {
   const organization = useOrganization();
   const originalTag = content.originalTag;
   const tagMeta = content.meta?.value?.[''];
   const tagErrors = tagMeta?.err ?? [];
-  const hasTagErrors = tagErrors.length > 0;
+  const hasTagErrors = tagErrors.length > 0 && !config?.disableActions;
 
   if (!originalTag) {
     return (
@@ -57,8 +65,31 @@ export default function EventTagsTreeRow({
       </TreeRow>
     );
   }
+  const tagValue =
+    originalTag.key === 'release' && !config?.disableRichValue ? (
+      <VersionHoverCard
+        organization={organization}
+        projectSlug={projectSlug}
+        releaseVersion={content.value}
+        showUnderline
+        underlineColor="linkUnderline"
+      >
+        <Version version={content.value} truncate />
+      </VersionHoverCard>
+    ) : (
+      <EventTagsValue tag={originalTag} meta={tagMeta} withOnlyFormattedText />
+    );
+
+  const tagActions = hasTagErrors ? (
+    <TreeValueErrors data-test-id="tag-tree-row-errors">
+      <AnnotatedTextErrors errors={tagErrors} />
+    </TreeValueErrors>
+  ) : (
+    <EventTagsTreeRowDropdown content={content} event={event} />
+  );
+
   return (
-    <TreeRow data-test-id="tag-tree-row" hasErrors={hasTagErrors}>
+    <TreeRow data-test-id="tag-tree-row" hasErrors={hasTagErrors} {...props}>
       <TreeKeyTrunk spacerCount={spacerCount}>
         {spacerCount > 0 && (
           <Fragment>
@@ -72,28 +103,8 @@ export default function EventTagsTreeRow({
         </TreeKey>
       </TreeKeyTrunk>
       <TreeValueTrunk>
-        <TreeValue>
-          {originalTag.key === 'release' ? (
-            <VersionHoverCard
-              organization={organization}
-              projectSlug={projectSlug}
-              releaseVersion={content.value}
-              showUnderline
-              underlineColor="linkUnderline"
-            >
-              <Version version={content.value} truncate />
-            </VersionHoverCard>
-          ) : (
-            <EventTagsValue tag={originalTag} meta={tagMeta} withOnlyFormattedText />
-          )}
-        </TreeValue>
-        {hasTagErrors ? (
-          <TreeValueErrors data-test-id="tag-tree-row-errors">
-            <AnnotatedTextErrors errors={tagErrors} />
-          </TreeValueErrors>
-        ) : (
-          <EventTagsTreeRowDropdown content={content} event={event} />
-        )}
+        <TreeValue hasErrors={hasTagErrors}>{tagValue}</TreeValue>
+        {!config?.disableActions && tagActions}
       </TreeValueTrunk>
     </TreeRow>
   );
@@ -212,6 +223,7 @@ const TreeRow = styled('div')<{hasErrors: boolean}>`
   display: grid;
   align-items: center;
   grid-column: span 2;
+  column-gap: ${space(1.5)};
   grid-template-columns: subgrid;
   :nth-child(odd) {
     background-color: ${p =>
@@ -270,17 +282,18 @@ const TreeValueTrunk = styled('div')`
   grid-column-gap: ${space(0.5)};
 `;
 
-const TreeValue = styled('div')`
+const TreeValue = styled('div')<{hasErrors?: boolean}>`
   padding: ${space(0.25)} 0;
   align-self: start;
   font-family: ${p => p.theme.text.familyMono};
   font-size: ${p => p.theme.fontSizeSmall};
   word-break: break-word;
   grid-column: span 1;
+  color: ${p => (p.hasErrors ? 'inherit' : p.theme.textColor)};
 `;
 
-const TreeKey = styled(TreeValue)<{hasErrors: boolean}>`
-  color: ${p => (p.hasErrors ? 'inherit' : p.theme.gray300)};
+const TreeKey = styled(TreeValue)<{hasErrors?: boolean}>`
+  color: ${p => (p.hasErrors ? 'inherit' : p.theme.subText)};
 `;
 
 /**

+ 2 - 2
static/app/components/events/eventTags/util.tsx

@@ -172,8 +172,8 @@ const ISSUE_DETAILS_COLUMN_BREAKPOINTS = [
  * rendered in the page contents, modals, and asides, we can't rely on window breakpoint to
  * accurately describe the available space.
  */
-export function useIssueDetailsColumnCount(containerRef: RefObject<HTMLElement>): number {
-  const {width} = useDimensions<HTMLElement>({elementRef: containerRef});
+export function useIssueDetailsColumnCount(elementRef: RefObject<HTMLElement>): number {
+  const {width} = useDimensions<HTMLElement>({elementRef});
   const breakPoint = ISSUE_DETAILS_COLUMN_BREAKPOINTS.find(
     ({minWidth}) => width >= minWidth
   );

+ 3 - 3
static/app/components/events/eventTagsAndScreenshot/index.spec.tsx

@@ -513,7 +513,7 @@ describe('EventTagsAndScreenshot', function () {
     });
   });
 
-  describe("renders changes for 'event-tags-new-ui' flag", function () {
+  describe("renders changes for 'event-tags-tree-ui' flag", function () {
     const featuredOrganization = OrganizationFixture({
       features: ['event-attachments', 'event-tags-tree-ui'],
     });
@@ -594,7 +594,7 @@ describe('EventTagsAndScreenshot', function () {
       assertFlagAndQueryParamWork();
     });
 
-    it("allows filtering with 'event-tags-new-ui' flag", async function () {
+    it("allows filtering with 'event-tags-tree-ui' flag", async function () {
       MockApiClient.addMockResponse({
         url: `/projects/${featuredOrganization.slug}/${project.slug}/events/${event.id}/attachments/`,
         body: [],
@@ -635,7 +635,7 @@ describe('EventTagsAndScreenshot', function () {
       expect(rows).toHaveLength(allTags.length);
     });
 
-    it("promotes custom tags with 'event-tags-new-ui' flag", async function () {
+    it("promotes custom tags with 'event-tags-tree-ui' flag", async function () {
       MockApiClient.addMockResponse({
         url: `/projects/${featuredOrganization.slug}/${project.slug}/events/${event.id}/attachments/`,
         body: [],

+ 135 - 0
static/app/components/events/highlights/util.spec.tsx

@@ -0,0 +1,135 @@
+import {EventFixture} from 'sentry-fixture/event';
+
+import {initializeOrg} from 'sentry-test/initializeOrg';
+
+import {
+  getHighlightContextData,
+  getHighlightTagData,
+} from 'sentry/components/events/highlights/util';
+
+const TEST_EVENT_CONTEXTS = {
+  keyboard: {
+    type: 'default',
+    brand: 'keychron',
+    percent: 75,
+    switches: {
+      form: 'tactile',
+      brand: 'wuque studios',
+    },
+  },
+  client_os: {
+    type: 'os',
+    name: 'Mac OS X',
+    version: '10.15',
+  },
+  runtime: {
+    type: 'runtime',
+    name: 'CPython',
+    version: '3.8.13',
+  },
+};
+
+const TEST_EVENT_TAGS = [
+  {
+    key: 'browser',
+    value: 'Chrome 1.2.3',
+  },
+  {
+    key: 'browser.name',
+    value: 'Chrome',
+  },
+  {
+    key: 'device.family',
+    value: 'Mac',
+  },
+  {
+    key: 'environment',
+    value: 'production',
+  },
+  {
+    key: 'handled',
+    value: 'no',
+  },
+  {
+    key: 'level',
+    value: 'error',
+  },
+  {
+    key: 'release',
+    value: '1.8',
+  },
+  {
+    key: 'runtime',
+    value: 'CPython 3.8.13',
+  },
+  {
+    key: 'runtime.name',
+    value: 'CPython',
+  },
+  {
+    key: 'url',
+    value: 'https://example.com',
+  },
+];
+
+describe('getHighlightContextData', function () {
+  it('returns only highlight context data', function () {
+    const {organization, project} = initializeOrg();
+    const event = EventFixture({
+      contexts: TEST_EVENT_CONTEXTS,
+    });
+    const highlightContext = {
+      keyboard: ['brand', 'switches'],
+    };
+    const highlightCtxData = getHighlightContextData({
+      event,
+      highlightContext,
+      project,
+      organization,
+    });
+    expect(highlightCtxData).toHaveLength(1);
+    expect(highlightCtxData[0].alias).toBe('keyboard');
+    expect(highlightCtxData[0].type).toBe('default');
+    expect(highlightCtxData[0].data).toHaveLength(highlightContext.keyboard.length);
+    const highlightCtxDataKeys = new Set(highlightCtxData[0].data.map(({key}) => key));
+    for (const ctxKey of highlightContext.keyboard) {
+      expect(highlightCtxDataKeys.has(ctxKey)).toBe(true);
+    }
+  });
+
+  it.each([
+    ['alias', {client_os: ['version']}],
+    ['type', {os: ['version']}],
+    ['title', {'Operating System': ['version']}],
+  ])('matches highlights on context %s', (_type, highlightContext) => {
+    const {organization, project} = initializeOrg();
+    const event = EventFixture({
+      contexts: TEST_EVENT_CONTEXTS,
+    });
+    const highlightCtxData = getHighlightContextData({
+      event,
+      highlightContext,
+      project,
+      organization,
+    });
+    expect(highlightCtxData).toHaveLength(1);
+    expect(highlightCtxData[0].type).toBe('os');
+  });
+});
+
+describe('getHighlightTagData', function () {
+  it('returns only highlight tag data', function () {
+    const event = EventFixture({
+      tags: TEST_EVENT_TAGS,
+    });
+    const highlightTags = ['release', 'url', 'environment'];
+    const highlightTagsSet = new Set(highlightTags);
+
+    const highlightTagData = getHighlightTagData({event, highlightTags});
+
+    expect(highlightTagData).toHaveLength(highlightTagsSet.size);
+    for (const content of highlightTagData) {
+      expect(highlightTagsSet.has(content.originalTag.key)).toBe(true);
+    }
+  });
+});

+ 103 - 0
static/app/components/events/highlights/util.tsx

@@ -0,0 +1,103 @@
+import {
+  type ContextItem,
+  getOrderedContextItems,
+} from 'sentry/components/events/contexts';
+import {
+  getContextTitle,
+  getFormattedContextData,
+} from 'sentry/components/events/contexts/utils';
+import type {TagTreeContent} from 'sentry/components/events/eventTags/eventTagsTree';
+import type {
+  Event,
+  EventTag,
+  KeyValueListData,
+  Organization,
+  Project,
+} from 'sentry/types';
+
+export type HighlightTags = Required<Project>['highlightTags'];
+export type HighlightContext = Required<Project>['highlightContext'];
+
+interface ContextData extends ContextItem {
+  data: KeyValueListData;
+}
+
+export function getHighlightContextData({
+  event,
+  highlightContext,
+  project,
+  organization,
+}: {
+  event: Event;
+  highlightContext: HighlightContext;
+  organization: Organization;
+  project: Project;
+}) {
+  const highlightContextSets: Record<ContextData['type'], Set<string>> = Object.entries(
+    highlightContext
+  ).reduce(
+    (hcSets, [contextType, contextKeys]) => ({
+      ...hcSets,
+      [contextType]: new Set(contextKeys),
+    }),
+    {}
+  );
+
+  const allContextData: ContextData[] = getOrderedContextItems(event).map(
+    ({alias, type, value}) => ({
+      alias,
+      type,
+      value,
+      data: getFormattedContextData({
+        event,
+        contextType: type,
+        contextValue: value,
+        organization,
+        project,
+      }),
+    })
+  );
+
+  const highlightContextData: ContextData[] = allContextData
+    .map(({alias, type: contextType, value, data}) => {
+      // Find the highlight key set for this type of context
+      // We match on alias (e.g. 'client_os'), type (e.g. 'os') and title (e.g. 'Operating System')
+      const highlightContextKeys =
+        highlightContextSets[alias] ??
+        highlightContextSets[contextType] ??
+        highlightContextSets[getContextTitle({alias, type: contextType, value})] ??
+        new Set([]);
+      // Filter data to only items from that set
+      const highlightContextItems: KeyValueListData = data.filter(
+        ({key, subject}) =>
+          // We match on key (e.g. 'trace_id') and subject (e.g. 'Trace ID')
+          highlightContextKeys.has(key) || highlightContextKeys.has(subject)
+      );
+      return {alias, type: contextType, data: highlightContextItems, value: value};
+    })
+    // Retain only entries with highlights
+    .filter(({data}) => data.length > 0);
+
+  return highlightContextData;
+}
+
+export function getHighlightTagData({
+  event,
+  highlightTags,
+}: {
+  event: Event;
+  highlightTags: HighlightTags;
+}): Required<TagTreeContent>[] {
+  const EMPTY_TAG_VALUE = '';
+  const tagMap: Record<string, {meta: Record<string, any>; tag: EventTag}> =
+    event.tags.reduce((tm, tag, i) => {
+      tm[tag.key] = {tag, meta: event._meta?.tags?.[i]};
+      return tm;
+    }, {});
+  return highlightTags.map(tagKey => ({
+    subtree: {},
+    meta: tagMap[tagKey]?.meta ?? {},
+    value: tagMap[tagKey]?.tag?.value ?? EMPTY_TAG_VALUE,
+    originalTag: tagMap[tagKey]?.tag ?? {key: tagKey, value: EMPTY_TAG_VALUE},
+  }));
+}

+ 1 - 1
static/app/types/event.tsx

@@ -652,7 +652,7 @@ export interface ResponseContext {
   type: 'response';
 }
 
-type EventContexts = {
+export type EventContexts = {
   'Memory Info'?: MemoryInfoContext;
   'ThreadPool Info'?: ThreadPoolInfoContext;
   browser?: BrowserContext;

Some files were not shown because too many files changed in this diff