Browse Source

feat(sampling): Add rules panel new design - (#36246)

Priscila Oliveira 2 years ago
parent
commit
41f7dc7dcc

+ 2 - 0
static/app/components/switchButton.tsx

@@ -28,6 +28,7 @@ const Switch = ({
   id,
   name,
   className,
+  ...props
 }: Props) => (
   <SwitchButton
     ref={forwardedRef}
@@ -43,6 +44,7 @@ const Switch = ({
     isActive={isActive}
     size={size}
     data-test-id="switch"
+    {...props}
   >
     <Toggle
       isDisabled={isDisabled}

+ 7 - 2
static/app/types/sampling.tsx

@@ -167,9 +167,14 @@ export type SamplingRule = {
    */
   type: SamplingRuleType;
   /**
-   * A rule can be disabled if it doesn't contain a condition (Else case)
+   * Indicates if the rule is enabled for server-side sampling
    */
-  disabled?: boolean;
+  active?: boolean;
+  /**
+   * A rule without a condition (Else case) always have to be 'pinned'
+   * to the bottom of the list and cannot be sorted.
+   */
+  bottomPinned?: boolean;
 };
 
 export type SamplingRules = Array<SamplingRule>;

+ 1 - 1
static/app/views/settings/project/sampling/rules/draggableList/index.tsx

@@ -74,7 +74,7 @@ export function DraggableList({
             id={itemId}
             index={index}
             renderItem={renderItem}
-            disabled={disabled || items[index].disabled}
+            disabled={disabled || items[index].bottomPinned}
             wrapperStyle={wrapperStyle}
           />
         ))}

+ 1 - 1
static/app/views/settings/project/sampling/rules/index.tsx

@@ -168,7 +168,7 @@ export class Rules extends PureComponent<Props, State> {
                     : SamplingRuleOperator.ELSE_IF
                 }
                 hideGrabButton={items.length === 1}
-                rule={{...currentRule, disabled: itemsRule.disabled}}
+                rule={{...currentRule, bottomPinned: itemsRule.bottomPinned}}
                 onEditRule={onEditRule(currentRule)}
                 onDeleteRule={onDeleteRule(currentRule)}
                 noPermission={disabled}

+ 1 - 1
static/app/views/settings/project/sampling/rules/rule/index.tsx

@@ -57,7 +57,7 @@ export function Rule({
   }, [dragging, sorting, state.isMenuActionsOpen]);
 
   return (
-    <Columns disabled={rule.disabled || noPermission}>
+    <Columns disabled={rule.bottomPinned || noPermission}>
       {hideGrabButton ? (
         <Column />
       ) : (

+ 2 - 2
static/app/views/settings/project/server-side-sampling/modals/uniformRateModal.tsx

@@ -16,7 +16,7 @@ import {defined} from 'sentry/utils';
 import {formatPercentage} from 'sentry/utils/formatters';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 
-import {SERVER_SIDE_DOC_LINK} from '../utils';
+import {SERVER_SIDE_SAMPLING_DOC_LINK} from '../utils';
 
 enum Strategy {
   CURRENT = 'current',
@@ -158,7 +158,7 @@ function UniformRateModal({Header, Body, Footer, closeModal}: Props) {
       </Body>
       <Footer>
         <FooterActions>
-          <Button href={SERVER_SIDE_DOC_LINK} external>
+          <Button href={SERVER_SIDE_SAMPLING_DOC_LINK} external>
             {t('Read Docs')}
           </Button>
 

+ 2 - 2
static/app/views/settings/project/server-side-sampling/promo.tsx

@@ -8,7 +8,7 @@ import EmptyStateWarning from 'sentry/components/emptyStateWarning';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
 
-import {SERVER_SIDE_DOC_LINK} from './utils';
+import {SERVER_SIDE_SAMPLING_DOC_LINK} from './utils';
 
 type Props = {
   hasAccess: boolean;
@@ -23,7 +23,7 @@ export function Promo({onGetStarted, hasAccess}: Props) {
         <h3>{t('No sampling rules active yet')}</h3>
         <p>{t('Set up your project for sampling success')}</p>
         <Actions gap={1}>
-          <Button href={SERVER_SIDE_DOC_LINK} external>
+          <Button href={SERVER_SIDE_SAMPLING_DOC_LINK} external>
             {t('Read Docs')}
           </Button>
           <Button

+ 293 - 0
static/app/views/settings/project/server-side-sampling/rule.tsx

@@ -0,0 +1,293 @@
+import {Fragment, useEffect, useState} from 'react';
+import {DraggableSyntheticListeners, UseDraggableArguments} from '@dnd-kit/core';
+import {css} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import MenuItemActionLink from 'sentry/components/actions/menuItemActionLink';
+import Button from 'sentry/components/button';
+import DropdownLink from 'sentry/components/dropdownLink';
+import NewBooleanField from 'sentry/components/forms/booleanField';
+import Tooltip from 'sentry/components/tooltip';
+import {IconDownload, IconEllipsis} from 'sentry/icons';
+import {IconGrabbable} from 'sentry/icons/iconGrabbable';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import {LegacyBrowser, SamplingRule, SamplingRuleOperator} from 'sentry/types/sampling';
+
+import {getInnerNameLabel, LEGACY_BROWSER_LIST} from './utils';
+
+type Props = {
+  dragging: boolean;
+  /**
+   * Hide the grab button if true.
+   * This is used when the list has a single item, making sorting not possible.
+   */
+  hideGrabButton: boolean;
+  listeners: DraggableSyntheticListeners;
+  noPermission: boolean;
+  onDeleteRule: () => void;
+  onEditRule: () => void;
+  operator: SamplingRuleOperator;
+  rule: SamplingRule;
+  sorting: boolean;
+  grabAttributes?: UseDraggableArguments['attributes'];
+};
+
+type State = {
+  isMenuActionsOpen: boolean;
+};
+
+export function Rule({
+  dragging,
+  sorting,
+  rule,
+  noPermission,
+  onEditRule,
+  onDeleteRule,
+  listeners,
+  operator,
+  grabAttributes,
+  hideGrabButton,
+}: Props) {
+  const [state, setState] = useState<State>({isMenuActionsOpen: false});
+
+  useEffect(() => {
+    if ((dragging || sorting) && state.isMenuActionsOpen) {
+      setState({isMenuActionsOpen: false});
+    }
+  }, [dragging, sorting, state.isMenuActionsOpen]);
+
+  return (
+    <Fragment>
+      <GrabColumn disabled={rule.bottomPinned || noPermission}>
+        {hideGrabButton ? null : (
+          <Tooltip
+            title={
+              noPermission
+                ? t('You do not have permission to reorder rules.')
+                : operator === SamplingRuleOperator.ELSE
+                ? t('Rules without conditions cannot be reordered.')
+                : undefined
+            }
+            containerDisplayMode="flex"
+          >
+            <IconGrabbableWrapper
+              {...listeners}
+              {...grabAttributes}
+              aria-label={dragging ? t('Drop Rule') : t('Drag Rule')}
+            >
+              <IconGrabbable />
+            </IconGrabbableWrapper>
+          </Tooltip>
+        )}
+      </GrabColumn>
+      <OperatorColumn>
+        <Operator>
+          {operator === SamplingRuleOperator.IF
+            ? t('If')
+            : operator === SamplingRuleOperator.ELSE_IF
+            ? t('Else if')
+            : t('Else')}
+        </Operator>
+      </OperatorColumn>
+      <ConditionColumn>
+        {hideGrabButton && !rule.condition.inner.length
+          ? t('All')
+          : rule.condition.inner.map((condition, index) => (
+              <Fragment key={index}>
+                <ConditionName>{getInnerNameLabel(condition.name)}</ConditionName>
+                <ConditionEqualOperator>{'='}</ConditionEqualOperator>
+                {Array.isArray(condition.value) ? (
+                  <div>
+                    {[...condition.value].map((conditionValue, conditionValueIndex) => (
+                      <Fragment key={conditionValue}>
+                        <ConditionValue>
+                          {LEGACY_BROWSER_LIST[conditionValue]?.title ?? conditionValue}
+                        </ConditionValue>
+                        {conditionValueIndex !==
+                          (condition.value as LegacyBrowser[]).length - 1 && (
+                          <ConditionSeparator>{'\u002C'}</ConditionSeparator>
+                        )}
+                      </Fragment>
+                    ))}
+                  </div>
+                ) : (
+                  <ConditionValue>
+                    {LEGACY_BROWSER_LIST[String(condition.value)]?.title ??
+                      String(condition.value)}
+                  </ConditionValue>
+                )}
+              </Fragment>
+            ))}
+      </ConditionColumn>
+      <RateColumn>
+        <SampleRate>{`${rule.sampleRate * 100}\u0025`}</SampleRate>
+      </RateColumn>
+      <ActiveColumn>
+        <ActiveToggle
+          inline={false}
+          hideControlState
+          aria-label={rule.active ? t('Deactivate Rule') : t('Activate Rule')}
+          name="active"
+        />
+      </ActiveColumn>
+      <Column>
+        <EllipisDropDownButton
+          caret={false}
+          customTitle={
+            <Button
+              aria-label={t('Actions')}
+              icon={<IconEllipsis />}
+              size="small"
+              onClick={() => {
+                setState({isMenuActionsOpen: !state.isMenuActionsOpen});
+              }}
+            />
+          }
+          isOpen={state.isMenuActionsOpen}
+          anchorRight
+        >
+          <MenuItemActionLink
+            shouldConfirm={false}
+            icon={<IconDownload size="xs" />}
+            title={t('Edit')}
+            onClick={
+              !noPermission
+                ? onEditRule
+                : event => {
+                    event?.stopPropagation();
+                  }
+            }
+            disabled={noPermission}
+          >
+            <Tooltip
+              disabled={!noPermission}
+              title={t('You do not have permission to edit sampling rules.')}
+              containerDisplayMode="block"
+            >
+              {t('Edit')}
+            </Tooltip>
+          </MenuItemActionLink>
+          <MenuItemActionLink
+            onAction={onDeleteRule}
+            message={t('Are you sure you wish to delete this sampling rule?')}
+            icon={<IconDownload size="xs" />}
+            title={t('Delete')}
+            disabled={noPermission}
+            priority="danger"
+            shouldConfirm
+          >
+            <Tooltip
+              disabled={!noPermission}
+              title={t('You do not have permission to delete sampling rules.')}
+              containerDisplayMode="block"
+            >
+              {t('Delete')}
+            </Tooltip>
+          </MenuItemActionLink>
+        </EllipisDropDownButton>
+      </Column>
+    </Fragment>
+  );
+}
+
+export const Column = styled('div')`
+  display: flex;
+  padding: ${space(1)} ${space(2)};
+  cursor: default;
+  white-space: pre-wrap;
+  word-break: break-all;
+`;
+
+export const GrabColumn = styled(Column)<{disabled?: boolean}>`
+  [role='button'] {
+    cursor: grab;
+  }
+
+  ${p =>
+    p.disabled &&
+    css`
+      [role='button'] {
+        cursor: not-allowed;
+      }
+      color: ${p.theme.disabled};
+    `}
+
+  display: none;
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    display: flex;
+  }
+`;
+
+export const OperatorColumn = styled(Column)`
+  display: none;
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    display: flex;
+  }
+`;
+
+export const ConditionColumn = styled(Column)`
+  display: flex;
+  gap: ${space(1)};
+  align-items: flex-start;
+  flex-wrap: wrap;
+`;
+
+export const RateColumn = styled(Column)`
+  justify-content: flex-end;
+  text-align: right;
+`;
+
+export const ActiveColumn = styled(Column)`
+  justify-content: center;
+  text-align: center;
+  display: none;
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    display: flex;
+  }
+`;
+
+const IconGrabbableWrapper = styled('div')`
+  outline: none;
+  display: flex;
+  align-items: center;
+  /* match the height of edit and delete buttons */
+  height: 34px;
+`;
+
+const ConditionEqualOperator = styled('div')`
+  color: ${p => p.theme.purple300};
+`;
+
+const Operator = styled('div')`
+  color: ${p => p.theme.active};
+`;
+
+const SampleRate = styled('div')`
+  white-space: pre-wrap;
+  word-break: break-all;
+`;
+
+const ActiveToggle = styled(NewBooleanField)`
+  padding: 0;
+  height: 34px;
+  justify-content: center;
+`;
+
+const ConditionName = styled('div')`
+  color: ${p => p.theme.gray400};
+`;
+
+const ConditionValue = styled('span')`
+  color: ${p => p.theme.gray300};
+`;
+
+const ConditionSeparator = styled(ConditionValue)`
+  padding-right: ${space(0.5)};
+`;
+
+const EllipisDropDownButton = styled(DropdownLink)`
+  display: flex;
+  align-items: center;
+  transition: none;
+`;

+ 173 - 42
static/app/views/settings/project/server-side-sampling/serverSideSampling.tsx

@@ -1,15 +1,23 @@
 import {Fragment, useEffect, useState} from 'react';
+import {css} from '@emotion/react';
 import styled from '@emotion/styled';
-import partition from 'lodash/partition';
 
 import {openModal} from 'sentry/actionCreators/modal';
+import Button from 'sentry/components/button';
+import ButtonBar from 'sentry/components/buttonBar';
 import LoadingError from 'sentry/components/loadingError';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
-import {PanelTable} from 'sentry/components/panels';
+import {Panel, PanelFooter, PanelHeader} from 'sentry/components/panels';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import {IconAdd} from 'sentry/icons';
 import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
 import {Project} from 'sentry/types';
-import {SamplingRules, SamplingRuleType} from 'sentry/types/sampling';
+import {
+  SamplingRuleOperator,
+  SamplingRules,
+  SamplingRuleType,
+} from 'sentry/types/sampling';
 import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -18,8 +26,20 @@ import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHea
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 import PermissionAlert from 'sentry/views/settings/organization/permissionAlert';
 
+import {DraggableList} from '../sampling/rules/draggableList';
+
 import {UniformRateModal} from './modals/uniformRateModal';
 import {Promo} from './promo';
+import {
+  ActiveColumn,
+  Column,
+  ConditionColumn,
+  GrabColumn,
+  OperatorColumn,
+  RateColumn,
+  Rule,
+} from './rule';
+import {SERVER_SIDE_SAMPLING_DOC_LINK} from './utils';
 
 export function ServerSideSampling() {
   const api = useApi();
@@ -29,7 +49,7 @@ export function ServerSideSampling() {
 
   const {orgId: orgSlug, projectId: projectSlug} = params;
 
-  const [_rules, setRules] = useState<SamplingRules>([]);
+  const [rules, setRules] = useState<SamplingRules>([]);
   const [project, setProject] = useState<Project>();
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState<string | undefined>(undefined);
@@ -45,18 +65,11 @@ export function ServerSideSampling() {
         const {dynamicSampling} = projectDetails;
         const samplingRules: SamplingRules = dynamicSampling?.rules ?? [];
 
-        const transactionRules = samplingRules.filter(
-          samplingRule =>
-            samplingRule.type === SamplingRuleType.TRANSACTION ||
-            samplingRule.type === SamplingRuleType.TRACE
-        );
-
-        const [rulesWithoutConditions, rulesWithConditions] = partition(
-          transactionRules,
-          transactionRule => !transactionRule.condition.inner.length
+        const traceRules = samplingRules.filter(
+          samplingRule => samplingRule.type === SamplingRuleType.TRACE
         );
 
-        setRules([...rulesWithConditions, ...rulesWithoutConditions]);
+        setRules(traceRules);
         setProject(projectDetails);
         setLoading(false);
       } catch (err) {
@@ -75,6 +88,14 @@ export function ServerSideSampling() {
     ));
   }
 
+  // Rules without a condition (Else case) always have to be 'pinned' to the bottom of the list
+  // and cannot be sorted
+  const items = rules.map(rule => ({
+    ...rule,
+    id: String(rule.id),
+    bottomPinned: !rule.condition.inner.length,
+  }));
+
   return (
     <SentryDocumentTitle title={t('Server-side Sampling')}>
       <Fragment>
@@ -93,10 +114,112 @@ export function ServerSideSampling() {
         {error && <LoadingError message={error} />}
         {!error && loading && <LoadingIndicator />}
         {!error && !loading && (
-          <RulesPanel
-            headers={['', t('Operator'), t('Condition'), t('Rate'), t('Active'), '']}
-          >
-            <Promo onGetStarted={handleGetStarted} hasAccess={hasAccess} />
+          <RulesPanel>
+            <RulesPanelHeader lightText>
+              <RulesPanelLayout>
+                <GrabColumn />
+                <OperatorColumn>{t('Operator')}</OperatorColumn>
+                <ConditionColumn>{t('Condition')}</ConditionColumn>
+                <RateColumn>{t('Rate')}</RateColumn>
+                <ActiveColumn>{t('Active')}</ActiveColumn>
+                <Column />
+              </RulesPanelLayout>
+            </RulesPanelHeader>
+            {!rules.length && (
+              <Promo onGetStarted={handleGetStarted} hasAccess={hasAccess} />
+            )}
+            {!!rules.length && (
+              <Fragment>
+                <DraggableList
+                  disabled={!hasAccess}
+                  items={items}
+                  onUpdateItems={() => {}}
+                  wrapperStyle={({isDragging, isSorting, index}) => {
+                    if (isDragging) {
+                      return {
+                        cursor: 'grabbing',
+                      };
+                    }
+                    if (isSorting) {
+                      return {};
+                    }
+                    return {
+                      transform: 'none',
+                      transformOrigin: '0',
+                      '--box-shadow': 'none',
+                      '--box-shadow-picked-up': 'none',
+                      overflow: 'visible',
+                      position: 'relative',
+                      zIndex: rules.length - index,
+                      cursor: 'default',
+                    };
+                  }}
+                  renderItem={({value, listeners, attributes, dragging, sorting}) => {
+                    const itemsRuleIndex = items.findIndex(item => item.id === value);
+
+                    if (itemsRuleIndex === -1) {
+                      return null;
+                    }
+
+                    const itemsRule = items[itemsRuleIndex];
+
+                    const currentRule = {
+                      active: itemsRule.active,
+                      condition: itemsRule.condition,
+                      sampleRate: itemsRule.sampleRate,
+                      type: itemsRule.type,
+                      id: Number(itemsRule.id),
+                    };
+
+                    return (
+                      <RulesPanelLayout isContent>
+                        <Rule
+                          operator={
+                            itemsRule.id === items[0].id
+                              ? SamplingRuleOperator.IF
+                              : itemsRule.bottomPinned
+                              ? SamplingRuleOperator.ELSE
+                              : SamplingRuleOperator.ELSE_IF
+                          }
+                          hideGrabButton={items.length === 1}
+                          rule={{
+                            ...currentRule,
+                            bottomPinned: itemsRule.bottomPinned,
+                          }}
+                          onEditRule={() => {}}
+                          onDeleteRule={() => {}}
+                          noPermission={!hasAccess}
+                          listeners={listeners}
+                          grabAttributes={attributes}
+                          dragging={dragging}
+                          sorting={sorting}
+                        />
+                      </RulesPanelLayout>
+                    );
+                  }}
+                />
+                <RulesPanelFooter>
+                  <ButtonBar gap={1}>
+                    <Button href={SERVER_SIDE_SAMPLING_DOC_LINK} external>
+                      {t('Read Docs')}
+                    </Button>
+                    <AddRuleButton
+                      disabled={!hasAccess}
+                      title={
+                        !hasAccess
+                          ? t("You don't have permission to add a rule")
+                          : undefined
+                      }
+                      priority="primary"
+                      onClick={() => {}}
+                      icon={<IconAdd isCircled />}
+                    >
+                      {t('Add Rule')}
+                    </AddRuleButton>
+                  </ButtonBar>
+                </RulesPanelFooter>
+              </Fragment>
+            )}
           </RulesPanel>
         )}
       </Fragment>
@@ -104,36 +227,44 @@ export function ServerSideSampling() {
   );
 }
 
-const RulesPanel = styled(PanelTable)`
-  > * {
-    :not(:last-child) {
-      border-bottom: 1px solid ${p => p.theme.border};
-    }
-
-    :nth-child(-n + 6):nth-child(6n - 1) {
-      text-align: right;
-    }
+const RulesPanel = styled(Panel)``;
 
-    @media (max-width: ${p => p.theme.breakpoints.small}) {
-      :nth-child(6n - 1),
-      :nth-child(6n - 4),
-      :nth-child(6n - 5) {
-        display: none;
-      }
-    }
-  }
+const RulesPanelHeader = styled(PanelHeader)`
+  padding: ${space(0.5)} 0;
+  font-size: ${p => p.theme.fontSizeSmall};
+`;
 
-  grid-template-columns: 1fr 0.5fr 66px;
+const RulesPanelLayout = styled('div')<{isContent?: boolean}>`
+  width: 100%;
+  display: grid;
+  grid-template-columns: 1fr 0.5fr 74px;
 
   @media (min-width: ${p => p.theme.breakpoints.small}) {
-    grid-template-columns: 48px 95px 1fr 0.5fr 77px 66px;
+    grid-template-columns: 48px 95px 1fr 0.5fr 77px 74px;
   }
 
-  @media (min-width: ${p => p.theme.breakpoints.large}) {
-    grid-template-columns: 48px 95px 1.5fr 1fr 77px 124px;
-  }
+  ${p =>
+    p.isContent &&
+    css`
+      > * {
+        /* match the height of the ellipsis button */
+        line-height: 34px;
+        border-bottom: 1px solid ${p.theme.border};
+      }
+    `}
+`;
+
+const RulesPanelFooter = styled(PanelFooter)`
+  border-top: none;
+  padding: ${space(1.5)} ${space(2)};
+  grid-column: 1 / -1;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+`;
 
-  @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
-    grid-template-columns: 48px 95px 1fr 0.5fr 77px 124px;
+const AddRuleButton = styled(Button)`
+  @media (max-width: ${p => p.theme.breakpoints.small}) {
+    width: 100%;
   }
 `;

+ 93 - 2
static/app/views/settings/project/server-side-sampling/utils.tsx

@@ -1,3 +1,94 @@
-// TODO(sampling): Update this link as soon as we have one for sampling
-export const SERVER_SIDE_DOC_LINK =
+import {t} from 'sentry/locale';
+import {LegacyBrowser, SamplingInnerName} from 'sentry/types/sampling';
+
+// TODO: Update this link as soon as we have one for sampling
+export const SERVER_SIDE_SAMPLING_DOC_LINK =
   'https://docs.sentry.io/product/data-management-settings/filtering/';
+
+const CUSTOM_TAG_PREFIX = 'event.tags.';
+
+export function stripCustomTagPrefix(name: string): string {
+  if (name.startsWith(CUSTOM_TAG_PREFIX)) {
+    return name.replace(CUSTOM_TAG_PREFIX, '');
+  }
+
+  return name;
+}
+
+export const LEGACY_BROWSER_LIST = {
+  [LegacyBrowser.IE_PRE_9]: {
+    icon: 'internet-explorer',
+    title: t('Internet Explorer version 8 and lower'),
+  },
+  [LegacyBrowser.IE9]: {
+    icon: 'internet-explorer',
+    title: t('Internet Explorer version 9'),
+  },
+  [LegacyBrowser.IE10]: {
+    icon: 'internet-explorer',
+    title: t('Internet Explorer version 10'),
+  },
+  [LegacyBrowser.IE11]: {
+    icon: 'internet-explorer',
+    title: t('Internet Explorer version 11'),
+  },
+  [LegacyBrowser.SAFARI_PRE_6]: {
+    icon: 'safari',
+    title: t('Safari version 5 and lower'),
+  },
+  [LegacyBrowser.OPERA_PRE_15]: {
+    icon: 'opera',
+    title: t('Opera version 14 and lower'),
+  },
+  [LegacyBrowser.OPERA_MINI_PRE_8]: {
+    icon: 'opera',
+    title: t('Opera Mini version 8 and lower'),
+  },
+  [LegacyBrowser.ANDROID_PRE_4]: {
+    icon: 'android',
+    title: t('Android version 3 and lower'),
+  },
+};
+
+export function getInnerNameLabel(name: SamplingInnerName | string) {
+  switch (name) {
+    case SamplingInnerName.TRACE_ENVIRONMENT:
+    case SamplingInnerName.EVENT_ENVIRONMENT:
+      return t('Environment');
+    case SamplingInnerName.TRACE_RELEASE:
+    case SamplingInnerName.EVENT_RELEASE:
+      return t('Release');
+    case SamplingInnerName.EVENT_USER_ID:
+    case SamplingInnerName.TRACE_USER_ID:
+      return t('User Id');
+    case SamplingInnerName.EVENT_USER_SEGMENT:
+    case SamplingInnerName.TRACE_USER_SEGMENT:
+      return t('User Segment');
+    case SamplingInnerName.EVENT_LOCALHOST:
+      return t('Localhost');
+    case SamplingInnerName.EVENT_WEB_CRAWLERS:
+      return t('Web Crawlers');
+    case SamplingInnerName.EVENT_LEGACY_BROWSER:
+      return t('Legacy Browser');
+    case SamplingInnerName.EVENT_TRANSACTION:
+    case SamplingInnerName.TRACE_TRANSACTION:
+      return t('Transaction');
+    case SamplingInnerName.EVENT_CSP:
+      return t('Content Security Policy');
+    case SamplingInnerName.EVENT_IP_ADDRESSES:
+      return t('IP Address');
+    case SamplingInnerName.EVENT_OS_NAME:
+      return t('OS Name');
+    case SamplingInnerName.EVENT_OS_VERSION:
+      return t('OS Version');
+    case SamplingInnerName.EVENT_DEVICE_FAMILY:
+      return t('Device Family');
+    case SamplingInnerName.EVENT_DEVICE_NAME:
+      return t('Device Name');
+    case SamplingInnerName.EVENT_CUSTOM_TAG:
+      return t('Add Custom Tag');
+
+    default:
+      return `${stripCustomTagPrefix(name)} - ${t('Custom')}`;
+  }
+}

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