Browse Source

feat(ai-monitoring): Add relevant graphs to AI-related issues (#69868)

<img width="880" alt="Screenshot 2024-04-29 at 11 13 23 AM"
src="https://github.com/getsentry/sentry/assets/161344340/8dfb113e-7fba-4e15-913a-a0758d39397d">

---------

Co-authored-by: Scott Cooper <scttcper@gmail.com>
colin-sentry 10 months ago
parent
commit
f79628c5d3

+ 72 - 0
static/app/components/events/interfaces/ai-monitoring/aiMonitoringSection.tsx

@@ -0,0 +1,72 @@
+import Alert from 'sentry/components/alert';
+import {LinkButton} from 'sentry/components/button';
+import ButtonBar from 'sentry/components/buttonBar';
+import {EventDataSection} from 'sentry/components/events/eventDataSection';
+import {IconOpen} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import type {Event} from 'sentry/types/event';
+import type {Organization} from 'sentry/types/organization';
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import {
+  NumberOfPipelinesChart,
+  TotalTokensUsedChart,
+} from 'sentry/views/aiMonitoring/aiMonitoringCharts';
+import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
+import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans';
+import {type IndexedResponse, SpanIndexedField} from 'sentry/views/starfish/types';
+
+interface Props {
+  event: Event;
+  organization: Organization;
+}
+
+export default function AIMonitoringSection({event, organization}: Props) {
+  const traceId = event.contexts.trace?.trace_id;
+  const spanId = event.contexts.trace?.span_id;
+  const {data, error, isLoading} = useIndexedSpans({
+    limit: 1,
+    fields: [SpanIndexedField.SPAN_AI_PIPELINE_GROUP],
+    referrer: 'api.ai-pipelines.view',
+    search: new MutableSearch(`trace:${traceId} id:"${spanId}"`),
+  });
+  const aiPipelineGroup =
+    data && (data[0] as IndexedResponse)?.[SpanIndexedField.SPAN_AI_PIPELINE_GROUP];
+
+  const actions = (
+    <ButtonBar gap={1}>
+      <LinkButton
+        size="xs"
+        icon={<IconOpen />}
+        to={`/organizations/${organization.slug}/ai-monitoring/`}
+      >
+        {t('View in AI Monitoring')}
+      </LinkButton>
+    </ButtonBar>
+  );
+
+  return (
+    <EventDataSection
+      title={t('AI monitoring')}
+      type="ai-monitoring"
+      help={t('Charts showing how many tokens are being used')}
+      actions={actions}
+    >
+      {error ? (
+        <Alert type="error" showIcon>
+          {'' + error}
+        </Alert>
+      ) : isLoading ? (
+        'loading'
+      ) : (
+        <ModuleLayout.Layout>
+          <ModuleLayout.Half>
+            <TotalTokensUsedChart groupId={aiPipelineGroup} />
+          </ModuleLayout.Half>
+          <ModuleLayout.Half>
+            <NumberOfPipelinesChart groupId={aiPipelineGroup} />
+          </ModuleLayout.Half>
+        </ModuleLayout.Layout>
+      )}
+    </EventDataSection>
+  );
+}

+ 36 - 3
static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx

@@ -1,4 +1,4 @@
-import {Fragment, useRef} from 'react';
+import {Fragment, lazy, useRef} from 'react';
 import styled from '@emotion/styled';
 
 import {CommitRow} from 'sentry/components/commitRow';
@@ -36,10 +36,18 @@ import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration';
 import {DataSection} from 'sentry/components/events/styles';
 import {SuspectCommits} from 'sentry/components/events/suspectCommits';
 import {EventUserFeedback} from 'sentry/components/events/userFeedback';
+import LazyLoad from 'sentry/components/lazyLoad';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import type {Event, Group, Project} from 'sentry/types';
-import {IssueCategory, IssueType} from 'sentry/types';
+import {
+  type EntryException,
+  type Event,
+  EventOrGroupType,
+  type Group,
+  IssueCategory,
+  IssueType,
+  type Project,
+} from 'sentry/types';
 import type {EventTransaction} from 'sentry/types/event';
 import {EntryType} from 'sentry/types/event';
 import {shouldShowCustomErrorResourceConfig} from 'sentry/utils/issueTypeConfig';
@@ -47,6 +55,10 @@ import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent';
 import useOrganization from 'sentry/utils/useOrganization';
 import {ResourcesAndMaybeSolutions} from 'sentry/views/issueDetails/resourcesAndMaybeSolutions';
 
+const AIMonitoringSection = lazy(
+  () => import('sentry/components/events/interfaces/ai-monitoring/aiMonitoringSection')
+);
+
 type GroupEventDetailsContentProps = {
   group: Group;
   project: Project;
@@ -124,6 +136,27 @@ function DefaultGroupEventDetailsContent({
           />
         </EventDataSection>
       )}
+      {event.type === EventOrGroupType.ERROR &&
+      organization.features.includes('ai-analytics') &&
+      event?.entries
+        ?.filter((x): x is EntryException => x.type === EntryType.EXCEPTION)
+        .flatMap(x => x.data.values ?? [])
+        .some(({value}) => {
+          const lowerText = value.toLowerCase();
+          return (
+            (lowerText.includes('api key') || lowerText.includes('429')) &&
+            (lowerText.includes('openai') ||
+              lowerText.includes('anthropic') ||
+              lowerText.includes('cohere') ||
+              lowerText.includes('langchain'))
+          );
+        }) ? (
+        <LazyLoad
+          LazyComponent={AIMonitoringSection}
+          event={event}
+          organization={organization}
+        />
+      ) : null}
       {group.issueCategory === IssueCategory.CRON && (
         <CronTimelineSection
           event={event}

+ 2 - 0
static/app/views/starfish/types.tsx

@@ -155,6 +155,7 @@ export enum SpanIndexedField {
   SPAN_OP = 'span.op',
   ID = 'span_id',
   SPAN_ACTION = 'span.action',
+  SPAN_AI_PIPELINE_GROUP = 'span.ai.pipeline.group',
   TRACE = 'trace',
   TRANSACTION_ID = 'transaction.id',
   TRANSACTION_METHOD = 'transaction.method',
@@ -189,6 +190,7 @@ export type IndexedResponse = {
   [SpanIndexedField.SPAN_MODULE]: string;
   [SpanIndexedField.SPAN_DESCRIPTION]: string;
   [SpanIndexedField.SPAN_OP]: string;
+  [SpanIndexedField.SPAN_AI_PIPELINE_GROUP]: string;
   [SpanIndexedField.ID]: string;
   [SpanIndexedField.SPAN_ACTION]: string;
   [SpanIndexedField.TRACE]: string;