Browse Source

feat(discover): Track Query/Dashboard visits in frontend (#28238)

Add an API call to track Dashboard and query visits
in the frontend.
Shruthi 3 years ago
parent
commit
c8668426df

+ 15 - 0
static/app/actionCreators/dashboards.tsx

@@ -32,6 +32,21 @@ export function createDashboard(
   return promise;
 }
 
+export function updateDashboardVisit(
+  api: Client,
+  orgId: string,
+  dashboardId: string | string[]
+): Promise<void> {
+  const promise = api.requestPromise(
+    `/organizations/${orgId}/dashboards/${dashboardId}/visit/`,
+    {
+      method: 'POST',
+    }
+  );
+
+  return promise;
+}
+
 export function fetchDashboard(
   api: Client,
   orgId: string,

+ 16 - 0
static/app/actionCreators/discoverSavedQueries.tsx

@@ -78,6 +78,22 @@ export function updateSavedQuery(
   return promise;
 }
 
+export function updateSavedQueryVisit(
+  orgId: string,
+  queryId: string | string[]
+): Promise<void> {
+  // Create a new client so the request is not cancelled
+  const api = new Client();
+  const promise = api.requestPromise(
+    `/organizations/${orgId}/discover/saved/${queryId}/visit/`,
+    {
+      method: 'POST',
+    }
+  );
+
+  return promise;
+}
+
 export function deleteSavedQuery(
   api: Client,
   orgId: string,

+ 12 - 2
static/app/views/dashboardsV2/view.tsx

@@ -1,6 +1,7 @@
-import React from 'react';
+import React, {useEffect} from 'react';
 import {RouteComponentProps} from 'react-router';
 
+import {updateDashboardVisit} from 'app/actionCreators/dashboards';
 import {Client} from 'app/api';
 import Feature from 'app/components/acl/feature';
 import Alert from 'app/components/alert';
@@ -23,7 +24,16 @@ type Props = RouteComponentProps<{orgId: string; dashboardId: string}, {}> & {
 };
 
 function ViewEditDashboard(props: Props) {
-  const {organization, params, api, location} = props;
+  const {api, organization, params, location} = props;
+  const dashboardId = params.dashboardId;
+  const orgSlug = organization.slug;
+
+  useEffect(() => {
+    if (dashboardId && dashboardId !== 'default-overview') {
+      updateDashboardVisit(api, orgSlug, dashboardId);
+    }
+  }, [api, orgSlug, dashboardId]);
+
   return (
     <DashboardBasicFeature organization={organization}>
       <OrgDashboards

+ 6 - 2
static/app/views/eventsV2/results.tsx

@@ -6,6 +6,7 @@ import {Location} from 'history';
 import isEqual from 'lodash/isEqual';
 import omit from 'lodash/omit';
 
+import {updateSavedQueryVisit} from 'app/actionCreators/discoverSavedQueries';
 import {fetchTotalCount} from 'app/actionCreators/events';
 import {fetchProjectsCount} from 'app/actionCreators/projects';
 import {loadOrganizationTags} from 'app/actionCreators/tags';
@@ -27,7 +28,7 @@ import ConfigStore from 'app/stores/configStore';
 import {PageContent} from 'app/styles/organization';
 import space from 'app/styles/space';
 import {GlobalSelection, Organization, SavedQuery} from 'app/types';
-import {generateQueryWithTag} from 'app/utils';
+import {defined, generateQueryWithTag} from 'app/utils';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
 import EventView, {isAPIPayloadSimilar} from 'app/utils/discover/eventView';
 import {generateAggregateFields} from 'app/utils/discover/fields';
@@ -100,11 +101,14 @@ class Results extends React.Component<Props, State> {
   };
 
   componentDidMount() {
-    const {organization, selection} = this.props;
+    const {organization, selection, location} = this.props;
     loadOrganizationTags(this.tagsApi, organization.slug, selection);
     addRoutePerformanceContext(selection);
     this.checkEventView();
     this.canLoadEvents();
+    if (defined(location.query.id)) {
+      updateSavedQueryVisit(organization.slug, location.query.id);
+    }
   }
 
   componentDidUpdate(prevProps: Props, prevState: State) {

+ 20 - 5
tests/js/spec/views/dashboardsV2/detail.spec.jsx

@@ -16,7 +16,7 @@ describe('Dashboards > Detail', function () {
 
   describe('prebuilt dashboards', function () {
     let wrapper;
-    let initialData;
+    let initialData, mockVisit;
 
     beforeEach(function () {
       initialData = initializeOrg({organization});
@@ -40,6 +40,12 @@ describe('Dashboards > Detail', function () {
         url: '/organizations/org-slug/dashboards/default-overview/',
         body: TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
       });
+      mockVisit = MockApiClient.addMockResponse({
+        url: '/organizations/org-slug/dashboards/1/visit/',
+        method: 'POST',
+        body: [],
+        statusCode: 200,
+      });
     });
 
     afterEach(function () {
@@ -158,13 +164,12 @@ describe('Dashboards > Detail', function () {
         .find('Controls Button[data-test-id="dashboard-edit"]')
         .props();
       expect(editProps.disabled).toBe(true);
+      expect(mockVisit).not.toHaveBeenCalled();
     });
   });
 
   describe('custom dashboards', function () {
-    let wrapper;
-    let initialData;
-    let widgets;
+    let wrapper, initialData, widgets, mockVisit;
 
     beforeEach(function () {
       initialData = initializeOrg({organization});
@@ -200,7 +205,12 @@ describe('Dashboards > Detail', function () {
           }
         ),
       ];
-
+      mockVisit = MockApiClient.addMockResponse({
+        url: '/organizations/org-slug/dashboards/1/visit/',
+        method: 'POST',
+        body: [],
+        statusCode: 200,
+      });
       MockApiClient.addMockResponse({
         url: '/organizations/org-slug/tags/',
         body: [],
@@ -247,6 +257,8 @@ describe('Dashboards > Detail', function () {
       await tick();
       wrapper.update();
 
+      expect(mockVisit).toHaveBeenCalledTimes(1);
+
       // Enter edit mode.
       wrapper.find('Controls Button[data-test-id="dashboard-edit"]').simulate('click');
 
@@ -279,6 +291,9 @@ describe('Dashboards > Detail', function () {
           }),
         })
       );
+
+      // Visit should not be called again on dashboard update
+      expect(mockVisit).toHaveBeenCalledTimes(1);
     });
 
     it('can enter edit mode for widgets', async function () {

+ 12 - 1
tests/js/spec/views/eventsV2/results.spec.jsx

@@ -28,7 +28,7 @@ const generateFields = () => ({
 describe('EventsV2 > Results', function () {
   const eventTitle = 'Oh no something bad';
   const features = ['discover-basic'];
-  let eventResultsMock, mockSaved, eventsStatsMock;
+  let eventResultsMock, mockSaved, eventsStatsMock, mockVisit;
 
   beforeEach(function () {
     MockApiClient.addMockResponse({
@@ -120,6 +120,12 @@ describe('EventsV2 > Results', function () {
         },
       ],
     });
+    mockVisit = MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/discover/saved/1/visit/',
+      method: 'POST',
+      body: [],
+      statusCode: 200,
+    });
     mockSaved = MockApiClient.addMockResponse({
       url: '/organizations/org-slug/discover/saved/1/',
       method: 'GET',
@@ -285,6 +291,9 @@ describe('EventsV2 > Results', function () {
     });
     await tick();
 
+    // should only be called with saved queries
+    expect(mockVisit).not.toHaveBeenCalled();
+
     // cursor query string should be omitted from the query string
     expect(initialData.router.push).toHaveBeenCalledWith({
       pathname: undefined,
@@ -583,6 +592,7 @@ describe('EventsV2 > Results', function () {
     expect(savedQuery.projects).toEqual([]);
     expect(savedQuery.range).toEqual('24h');
     expect(mockSaved).toHaveBeenCalled();
+    expect(mockVisit).toHaveBeenCalledTimes(1);
     wrapper.unmount();
   });
 
@@ -658,6 +668,7 @@ describe('EventsV2 > Results', function () {
     expect(eventView.project).toEqual([2]);
     expect(eventView.statsPeriod).toEqual('7d');
     expect(eventView.environment).toEqual(['production']);
+    expect(mockVisit).toHaveBeenCalledTimes(1);
     wrapper.unmount();
   });