Browse Source

feat(dam): Metrics meta reflux store (#32069)

Adds metrics meta to reflux store.
Shruthi 3 years ago
parent
commit
dc89ced396

+ 6 - 1
static/app/actionCreators/metrics.tsx

@@ -1,4 +1,5 @@
 import {addErrorMessage} from 'sentry/actionCreators/indicator';
+import MetricsMetaActions from 'sentry/actions/metricsMetaActions';
 import MetricsTagActions from 'sentry/actions/metricTagActions';
 import {Client} from 'sentry/api';
 import {getInterval} from 'sentry/components/charts/utils';
@@ -109,6 +110,10 @@ export function fetchMetricsTags(
   return promise;
 }
 
+function metaFetchSuccess(metricsMeta: MetricMeta[]) {
+  MetricsMetaActions.loadMetricsMetaSuccess(metricsMeta);
+}
+
 export function fetchMetricsFields(
   api: Client,
   orgSlug: Organization['slug'],
@@ -123,7 +128,7 @@ export function fetchMetricsFields(
     }
   );
 
-  promise.catch(response => {
+  promise.then(metaFetchSuccess).catch(response => {
     const errorResponse = response?.responseJSON ?? t('Unable to fetch metric fields');
     addErrorMessage(errorResponse);
     handleXhrErrorResponse(errorResponse)(response);

+ 5 - 0
static/app/actions/metricsMetaActions.tsx

@@ -0,0 +1,5 @@
+import Reflux from 'reflux';
+
+const MetricsMetaActions = Reflux.createActions(['loadMetricsMetaSuccess']);
+
+export default MetricsMetaActions;

+ 18 - 25
static/app/components/modals/addDashboardWidgetModal.tsx

@@ -9,7 +9,6 @@ import set from 'lodash/set';
 
 import {validateWidget} from 'sentry/actionCreators/dashboards';
 import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
-import {fetchMetricsFields} from 'sentry/actionCreators/metrics';
 import {ModalRenderProps} from 'sentry/actionCreators/modal';
 import {Client} from 'sentry/api';
 import Button from 'sentry/components/button';
@@ -26,7 +25,7 @@ import {t, tct} from 'sentry/locale';
 import space from 'sentry/styles/space';
 import {
   DateString,
-  MetricMeta,
+  MetricsMetaCollection,
   MetricTagCollection,
   Organization,
   PageFilters,
@@ -38,6 +37,7 @@ import Measurements from 'sentry/utils/measurements/measurements';
 import {SessionMetric} from 'sentry/utils/metrics/fields';
 import {SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/performance/spanOperationBreakdowns/constants';
 import withApi from 'sentry/utils/withApi';
+import withMetricsMeta from 'sentry/utils/withMetricsMeta';
 import withMetricsTags from 'sentry/utils/withMetricsTags';
 import withPageFilters from 'sentry/utils/withPageFilters';
 import withTags from 'sentry/utils/withTags';
@@ -91,6 +91,7 @@ export type DashboardWidgetModalOptions = {
 type Props = ModalRenderProps &
   DashboardWidgetModalOptions & {
     api: Client;
+    metricsMeta: MetricsMetaCollection;
     metricsTags: MetricTagCollection;
     organization: Organization;
     selection: PageFilters;
@@ -106,7 +107,6 @@ type State = {
   displayType: Widget['displayType'];
   interval: Widget['interval'];
   loading: boolean;
-  metricFields: MetricMeta[];
   queries: Widget['queries'];
   title: string;
   userHasModified: boolean;
@@ -163,7 +163,6 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
         errors: undefined,
         loading: !!this.omitDashboardProp,
         dashboards: [],
-        metricFields: [],
         userHasModified: false,
         widgetType: WidgetType.DISCOVER,
       };
@@ -178,7 +177,6 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
       errors: undefined,
       loading: false,
       dashboards: [],
-      metricFields: [],
       userHasModified: false,
       widgetType: widget.widgetType ?? WidgetType.DISCOVER,
     };
@@ -188,9 +186,6 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
     if (this.omitDashboardProp) {
       this.fetchDashboards();
     }
-    if (this.props.organization.features.includes('dashboards-metrics')) {
-      this.fetchMetricsFields();
-    }
   }
 
   get omitDashboardProp() {
@@ -525,19 +520,6 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
     this.setState({loading: false});
   }
 
-  async fetchMetricsFields() {
-    const {api, organization, selection} = this.props;
-    const projects = !selection.projects.length ? undefined : selection.projects;
-
-    const metricFields = await fetchMetricsFields(api, organization.slug, projects);
-    const filteredFields = metricFields.filter(field =>
-      METRICS_FIELDS_ALLOW_LIST.includes(field.name)
-    );
-    this.setState({
-      metricFields: filteredFields,
-    });
-  }
-
   handleDashboardChange(option: SelectValue<string>) {
     this.setState({selectedDashboard: option});
   }
@@ -596,8 +578,16 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
   }
 
   renderWidgetQueryForm() {
-    const {organization, selection, tags, metricsTags, start, end, statsPeriod} =
-      this.props;
+    const {
+      organization,
+      selection,
+      tags,
+      metricsTags,
+      metricsMeta,
+      start,
+      end,
+      statsPeriod,
+    } = this.props;
     const state = this.state;
     const errors = state.errors;
 
@@ -609,9 +599,12 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
       ? {...selection, datetime: {start, end, period: null, utc: null}}
       : selection;
 
+    const filteredMeta = Object.values(metricsMeta).filter(field =>
+      METRICS_FIELDS_ALLOW_LIST.includes(field.name)
+    );
     const issueWidgetFieldOptions = generateIssueWidgetFieldOptions();
     const metricsWidgetFieldOptions = generateMetricsWidgetFieldOptions(
-      state.metricFields.length ? state.metricFields : DEFAULT_METRICS_FIELDS,
+      filteredMeta.length ? filteredMeta : DEFAULT_METRICS_FIELDS,
       Object.values(metricsTags).map(({key}) => key)
     );
     const fieldOptions = (measurementKeys: string[]) =>
@@ -921,5 +914,5 @@ const StyledFieldLabel = styled(FieldLabel)`
 `;
 
 export default withApi(
-  withPageFilters(withTags(withMetricsTags(AddDashboardWidgetModal)))
+  withPageFilters(withTags(withMetricsMeta(withMetricsTags(AddDashboardWidgetModal))))
 );

+ 47 - 0
static/app/stores/metricsMetaStore.tsx

@@ -0,0 +1,47 @@
+import Reflux from 'reflux';
+
+import MetricsMetaActions from 'sentry/actions/metricsMetaActions';
+import {MetricMeta, MetricsMetaCollection} from 'sentry/types';
+
+type MetricsMetaStoreInterface = {
+  getAllFields(): MetricsMetaCollection;
+  onLoadSuccess(data: MetricMeta[]): void;
+  reset(): void;
+  state: MetricsMetaCollection;
+};
+
+const storeConfig: Reflux.StoreDefinition & MetricsMetaStoreInterface = {
+  state: {},
+
+  init() {
+    this.state = {};
+    this.listenTo(MetricsMetaActions.loadMetricsMetaSuccess, this.onLoadSuccess);
+  },
+
+  reset() {
+    this.state = {};
+    this.trigger(this.state);
+  },
+
+  getAllFields() {
+    return this.state;
+  },
+
+  onLoadSuccess(data) {
+    const newFields = data.reduce<MetricsMetaCollection>((acc, field) => {
+      acc[field.name] = {
+        ...field,
+      };
+
+      return acc;
+    }, {});
+
+    this.state = {...this.state, ...newFields};
+    this.trigger(this.state);
+  },
+};
+
+const MetricsMetaStore = Reflux.createStore(storeConfig) as Reflux.Store &
+  MetricsMetaStoreInterface;
+
+export default MetricsMetaStore;

+ 2 - 0
static/app/types/metrics.tsx

@@ -29,6 +29,8 @@ export type MetricMeta = {
   type: MetricsColumnType;
 };
 
+export type MetricsMetaCollection = Record<string, MetricMeta>;
+
 export type MetricQuery = {
   aggregation?: string;
   groupBy?: string[];

+ 49 - 0
static/app/utils/withMetricsMeta.tsx

@@ -0,0 +1,49 @@
+import * as React from 'react';
+
+import MetricsMetaStore from 'sentry/stores/metricsMetaStore';
+import {MetricsMetaCollection} from 'sentry/types';
+import getDisplayName from 'sentry/utils/getDisplayName';
+
+export type InjectedMetricsMetaProps = {
+  metricsMeta: MetricsMetaCollection;
+};
+
+type State = {
+  metricsMeta: MetricsMetaCollection;
+};
+
+function withMetricsMeta<P extends InjectedMetricsMetaProps>(
+  WrappedComponent: React.ComponentType<P>
+) {
+  class WithMetricMeta extends React.Component<
+    Omit<P, keyof InjectedMetricsMetaProps>,
+    State
+  > {
+    static displayName = `withMetricsMeta(${getDisplayName(WrappedComponent)})`;
+
+    state: State = {
+      metricsMeta: MetricsMetaStore.getAllFields(),
+    };
+
+    componentWillUnmount() {
+      this.unsubscribe();
+    }
+    unsubscribe = MetricsMetaStore.listen(
+      (metricsMeta: MetricsMetaCollection) => this.setState({metricsMeta}),
+      undefined
+    );
+
+    render() {
+      const {metricsMeta, ...props} = this.props as P;
+      return (
+        <WrappedComponent
+          {...({metricsMeta: metricsMeta ?? this.state.metricsMeta, ...props} as P)}
+        />
+      );
+    }
+  }
+
+  return WithMetricMeta;
+}
+
+export default withMetricsMeta;

+ 3 - 1
static/app/views/dashboardsV2/dashboard.tsx

@@ -16,7 +16,7 @@ import isEqual from 'lodash/isEqual';
 import {validateWidget} from 'sentry/actionCreators/dashboards';
 import {addErrorMessage} from 'sentry/actionCreators/indicator';
 import {fetchOrgMembers} from 'sentry/actionCreators/members';
-import {fetchMetricsTags} from 'sentry/actionCreators/metrics';
+import {fetchMetricsFields, fetchMetricsTags} from 'sentry/actionCreators/metrics';
 import {openAddDashboardWidgetModal} from 'sentry/actionCreators/modal';
 import {loadOrganizationTags} from 'sentry/actionCreators/tags';
 import {Client} from 'sentry/api';
@@ -144,12 +144,14 @@ class Dashboard extends Component<Props, State> {
     }
 
     if (organization.features.includes('dashboards-metrics')) {
+      fetchMetricsFields(api, organization.slug);
       fetchMetricsTags(api, organization.slug);
     }
     // Load organization tags when in edit mode.
     if (isEditing) {
       this.fetchTags();
     }
+
     this.addNewWidget();
 
     // Get member list data for issue widgets

+ 29 - 35
tests/js/spec/components/modals/addDashboardWidgetModal.spec.jsx

@@ -13,6 +13,7 @@ import {getOptionByLabel, openMenu, selectByLabel} from 'sentry-test/select-new'
 import {openDashboardWidgetLibraryModal} from 'sentry/actionCreators/modal';
 import AddDashboardWidgetModal from 'sentry/components/modals/addDashboardWidgetModal';
 import {t} from 'sentry/locale';
+import MetricsMetaStore from 'sentry/stores/metricsMetaStore';
 import MetricsTagStore from 'sentry/stores/metricsTagStore';
 import TagStore from 'sentry/stores/tagStore';
 import {SessionMetric} from 'sentry/utils/metrics/fields';
@@ -118,17 +119,44 @@ describe('Modals -> AddDashboardWidgetModal', function () {
     {name: 'custom-field', key: 'custom-field'},
   ];
   const metricsTags = [{key: 'environment'}, {key: 'release'}, {key: 'session.status'}];
+  const metricsMeta = [
+    {
+      name: 'sentry.sessions.session',
+      type: 'counter',
+      operations: ['sum'],
+      unit: null,
+    },
+    {
+      name: 'sentry.sessions.session.error',
+      type: 'set',
+      operations: ['count_unique'],
+      unit: null,
+    },
+    {
+      name: 'sentry.sessions.user',
+      type: 'set',
+      operations: ['count_unique'],
+      unit: null,
+    },
+    {
+      name: 'not.on.allow.list',
+      type: 'set',
+      operations: ['count_unique'],
+      unit: null,
+    },
+  ];
   const dashboard = TestStubs.Dashboard([], {
     id: '1',
     title: 'Test Dashboard',
     widgetDisplay: ['area'],
   });
 
-  let eventsStatsMock, metricsMetaMock, metricsDataMock;
+  let eventsStatsMock, metricsDataMock;
 
   beforeEach(function () {
     TagStore.onLoadTagsSuccess(tags);
     MetricsTagStore.onLoadTagsSuccess(metricsTags);
+    MetricsMetaStore.onLoadSuccess(metricsMeta);
     MockApiClient.addMockResponse({
       url: '/organizations/org-slug/dashboards/widgets/',
       method: 'POST',
@@ -163,35 +191,6 @@ describe('Modals -> AddDashboardWidgetModal', function () {
       url: '/organizations/org-slug/metrics/tags/',
       body: [{key: 'environment'}, {key: 'release'}, {key: 'session.status'}],
     });
-    metricsMetaMock = MockApiClient.addMockResponse({
-      url: '/organizations/org-slug/metrics/meta/',
-      body: [
-        {
-          name: 'sentry.sessions.session',
-          type: 'counter',
-          operations: ['sum'],
-          unit: null,
-        },
-        {
-          name: 'sentry.sessions.session.error',
-          type: 'set',
-          operations: ['count_unique'],
-          unit: null,
-        },
-        {
-          name: 'sentry.sessions.user',
-          type: 'set',
-          operations: ['count_unique'],
-          unit: null,
-        },
-        {
-          name: 'not.on.allow.list',
-          type: 'set',
-          operations: ['count_unique'],
-          unit: null,
-        },
-      ],
-    });
     metricsDataMock = MockApiClient.addMockResponse({
       method: 'GET',
       url: '/organizations/org-slug/metrics/data/',
@@ -1225,7 +1224,6 @@ describe('Modals -> AddDashboardWidgetModal', function () {
         source: types.DashboardWidgetSource.DASHBOARDS,
       });
 
-      expect(metricsMetaMock).not.toHaveBeenCalled();
       expect(metricsDataMock).not.toHaveBeenCalled();
 
       expect(screen.getByText('Data Set')).toBeInTheDocument();
@@ -1343,8 +1341,6 @@ describe('Modals -> AddDashboardWidgetModal', function () {
       });
 
       await tick();
-      expect(metricsMetaMock).toHaveBeenCalledTimes(1);
-
       await act(async () =>
         userEvent.click(screen.getByLabelText('Metrics (Release Health)'))
       );
@@ -1383,8 +1379,6 @@ describe('Modals -> AddDashboardWidgetModal', function () {
       });
 
       await tick();
-      expect(metricsMetaMock).toHaveBeenCalledTimes(1);
-
       await act(async () =>
         userEvent.click(screen.getByLabelText('Metrics (Release Health)'))
       );

+ 45 - 0
tests/js/spec/stores/metricsMetaStore.spec.tsx

@@ -0,0 +1,45 @@
+import MetricsMetaStore from 'sentry/stores/metricsMetaStore';
+
+describe('MetricsMetaStore', function () {
+  beforeEach(() => {
+    MetricsMetaStore.reset();
+  });
+
+  describe('onLoadSuccess()', () => {
+    it('should add a new fields and trigger the new addition', () => {
+      jest.spyOn(MetricsMetaStore, 'trigger');
+
+      const fields = MetricsMetaStore.getAllFields();
+      expect(fields).toEqual({});
+
+      MetricsMetaStore.onLoadSuccess([
+        {
+          name: 'sentry.sessions.session',
+          type: 'counter',
+          operations: ['sum'],
+        },
+        {
+          name: 'sentry.sessions.session.error',
+          type: 'set',
+          operations: ['count_unique'],
+        },
+      ]);
+
+      const updatedFields = MetricsMetaStore.getAllFields();
+      expect(updatedFields).toEqual({
+        'sentry.sessions.session': {
+          name: 'sentry.sessions.session',
+          type: 'counter',
+          operations: ['sum'],
+        },
+        'sentry.sessions.session.error': {
+          name: 'sentry.sessions.session.error',
+          type: 'set',
+          operations: ['count_unique'],
+        },
+      });
+
+      expect(MetricsMetaStore.trigger).toHaveBeenCalledTimes(1);
+    });
+  });
+});

+ 56 - 0
tests/js/spec/utils/withMetricsMeta.spec.tsx

@@ -0,0 +1,56 @@
+import {mountWithTheme, screen} from 'sentry-test/reactTestingLibrary';
+
+import MetricsMetaStore from 'sentry/stores/metricsMetaStore';
+import withMetricsMeta, {InjectedMetricsMetaProps} from 'sentry/utils/withMetricsMeta';
+
+interface MyComponentProps extends InjectedMetricsMetaProps {
+  other: string;
+}
+
+describe('withMetricsMeta HoC', function () {
+  beforeEach(() => {
+    MetricsMetaStore.reset();
+  });
+
+  it('works', function () {
+    jest.spyOn(MetricsMetaStore, 'trigger');
+    const MyComponent = (props: MyComponentProps) => {
+      return (
+        <div>
+          <span>{props.other}</span>
+          {props.metricsMeta &&
+            Object.entries(props.metricsMeta).map(([key, meta]) => (
+              <em key={key}>{meta.name}</em>
+            ))}
+        </div>
+      );
+    };
+
+    const Container = withMetricsMeta(MyComponent);
+    mountWithTheme(<Container other="value" />);
+
+    // Should forward props.
+    expect(screen.getByText('value')).toBeInTheDocument();
+
+    MetricsMetaStore.onLoadSuccess([
+      {
+        name: 'sentry.sessions.session',
+        type: 'counter',
+        operations: ['sum'],
+      },
+      {
+        name: 'sentry.sessions.session.error',
+        type: 'set',
+        operations: ['count_unique'],
+      },
+    ]);
+
+    // Should forward prop
+    expect(screen.getByText('value')).toBeInTheDocument();
+
+    expect(MetricsMetaStore.trigger).toHaveBeenCalledTimes(1);
+
+    expect(screen.getByText('sentry.sessions.session')).toBeInTheDocument();
+    expect(screen.getByText('sentry.sessions.session.error')).toBeInTheDocument();
+  });
+});