Skip to content

[App Search] Analytics - initial logic file & server API routes#88250

Merged
cee-chen merged 7 commits intoelastic:masterfrom
cee-chen:analytics-1
Jan 14, 2021
Merged

[App Search] Analytics - initial logic file & server API routes#88250
cee-chen merged 7 commits intoelastic:masterfrom
cee-chen:analytics-1

Conversation

@cee-chen
Copy link
Contributor

Summary

This PR adds an initial AnalyticsLogic (missing a ton of values - currently just a few basic ones in here + the initial API call) and our API routes for making a call to ent-search's analytics/query endpoints.

This PR cannot be tested by itself, only code reviewed - it will be QA-able in shortly-upcoming PRs that add actual Analytics views & functionality.

Checklist

- mostly just contains API call & basic loading/error state for now - actual usable vars will be defined in a future PR
@cee-chen cee-chen added Feature:Plugins release_note:skip Skip the PR/issue when compiling release notes v7.12.0 labels Jan 13, 2021
@cee-chen cee-chen requested review from a team, JasonStoltz and byronhulcher January 13, 2021 19:19
@cee-chen cee-chen changed the title [App Search] Initial AnalyticsLogic and server API routes [App Search] Analytics - initial logic file & server API routes Jan 13, 2021
Comment on lines +104 to +105
describe('onAnalyticsUnavailable', () => {
it('should set state', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JasonStoltz - I implemented the reducer block suggestions you noted in #87561 (comment)! I like this a lot so far, feels much faster 🎉

onAnalyticsDataLoad: (_, { analyticsUnavailable }) => analyticsUnavailable,
onQueryDataLoad: (_, { analyticsUnavailable }) => analyticsUnavailable,
},
],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a ton more reducers still to import, these are the primarily http/fetch-related ones that I know I'll be using soon.

I'm migrating reducers as-needed to try and catch ones we don't need and can potentially clean up from ent-search.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I like to migrate them as-needed as well.

actions.onAnalyticsUnavailable();
}
},
loadQueryData: async (query) => {
Copy link
Contributor Author

@cee-chen cee-chen Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadAnalyticsData and loadQueryData are fairly similar in structure - I debated trying to DRY them out (e.g., a shared fn that gets passed an API URL/query/callback), but in the end decided it wasn't worth it. We likely aren't ever going to have more than just these 2 API calls so I prioritized straightforward code over more abstract

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 I think that was a good choice

analyticsUnavailable: boolean;
allTags: string[];
// NOTE: The API sends us back even more data than this (e.g.,
// startDate, endDate, currentTag, logRetentionSettings, query),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dropped these from our types - logRetentionSettings and query weren't being used at all in the frontend, and I'm no longer using startDate, endDate, currentTag due to changes/refactors in our analytics header (different UI elements/components for the tags & datepicker).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, that comment is helpful.

Copy link
Contributor Author

@cee-chen cee-chen Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some day in the future, it would be nice to audit our server side API code and figure out what all we actually are using / aren't using and clean our responses up. I also might mention GraphQL unironically once or twice to give Byron hives


router.get(
{
path: '/api/app_search/engines/{engineName}/analytics/queries/{query}',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi I changed this slightly from the ent-search URL to be more RESTful (analytics/query/xyz->analytics/queries/xyz). It also follows the URL pattern we use for our documents API (e.g.engine/documents/id, not /engine/document/id).

Copy link
Member

@JasonStoltz JasonStoltz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with all of this. None of my comments are blocking. Proceed as you see fit.

analyticsUnavailable: false,
};

const MOCK_TOP_QUERIES = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found value in typing these sorts of default values for tests, it can catch stuff when the properties of these objects change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% convinced of this - our Kea logic should be type-checking for us and alerting if we have type issues. Not sure if ent-search has been updated to use makeLogicType, but in Kibana I definitely know it type checks my test files.

Comment on lines +296 to +301
try {
AnalyticsLogic.actions.loadQueryData('some-query');
await promise;
} catch {
// Do nothing
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above I'm not 100% you should be try/catch-ing here

Comment on lines +218 to +223
try {
AnalyticsLogic.actions.loadAnalyticsData();
await promise;
} catch {
// Do nothing
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect loadAnalyticsData to throw an Error the http fails? It looks like loadAnalyticsData is setup to catch the any http errors. Alternatively, I think you should explicitly assert that loadAnalyticsData is throwing.

Copy link
Contributor Author

@cee-chen cee-chen Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect loadAnalyticsData to throw an Error the http fails

Yes. Rejected promises as well as 400/500 responses from the server (in production) will trigger the catch block.

It looks like loadAnalyticsData is setup to catch the any http errors.

You can try this yourself if you pull the branch down, removing this try catch will cause the test to throw an error. I believe this actually might be because flashAPIErrors() re-throws the error if there isn't a valid message to display (we do this to catch Javascript syntax errors and other unexpected errors instead of silently swallowing them).

Alternatively, I think you should explicitly assert that loadAnalyticsData is throwing.

Unfortunately it's not quite that simple with Jest and async expects that are actually throws, the syntax is fairly bizarre and far more complex than this. I've struggled with this a lot in the past and am confident try/catch within the test is by far the easiest way to catch expected errors.

I definitely agree we could clarify the // Do nothing comment to be a little more helpful and indicate why we're doing nothing, although that would probably have to be a separate PR since I'm essentially copying a pattern we've done elsewhere in the codebase (originally set up by @JasonStoltz). We could also possibly extract this out to a helper which could help clear confusion in that regard.

Copy link
Member

@JasonStoltz JasonStoltz Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's complicated, IIRC.

You can't catch an exception thrown by a listener, like loadAnalyticsData.

I think it's cause you're not invoking the listener directly. They're on separate call stacks or something. It's like an evented system. Calling loadAnalyticsData broadcasts an event, and then 1 or more event listeners are triggered and execute. They're separate run contexts / call stacks / or however you would say that in JS. So you could wrap your code that triggers an event in a try / catch, and it will catch errors that occur while triggering that event, but it won't catch errors that occur in event handlers.

Does that much make sense?

So this code is actually misleading. You could actually move loadAnalyticsData outside of the try / catch and it would work ( I didn't test this, so please tell me to stuff it if I'm wrong 😰 )

        AnalyticsLogic.actions.loadAnalyticsData();
        
        try {
          await promise;
        } catch {
          // Do nothing
        }

So the point so far is that this test isn't actually testing that loadAnalyticsData throws correctly. This test is testing that the loadAnalyticsData correctly handles a rejected promise returned from http.get.

So here's the next challenge.

We need to tell http.get to return a reject promise so that we can test that scenario. And then we need to await that promise.

You can't do something like this:

(HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(Promise.reject());
await AnalyticsLogic.actions.loadAnalyticsData();

Because loadAnalyticsData doesn't return a promise. So we have to create the promise separately so we can hold a reference to it to await later:

        const promise = Promise.reject('error');
        (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise);
        await promise;

When you await a rejected promise, it will throw, so you gotta wrap it up:

        const promise = Promise.reject('error');
        (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise);

        try {
            await promise; 
        } catch {
          // Do nothing
        }

You may be tempted to run some assertions in the catch at this point..

        const promise = Promise.reject('error');
        (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise);

        try {
            await promise; 
        } catch {
          expect(flashAPIErrors).toHaveBeenCalledWith('error');
          expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled();
        }

But I think you should generally avoid that ... because if for some reason your code ever stops throwing, then your code won't run the expectations, but the test will still pass, so you end up with a false positive.

        const promise = Promise.reject('error');
        (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise);

        try {
            await promise; 
        } catch {
            // Do nothing
        }
        
          expect(flashAPIErrors).toHaveBeenCalledWith('error');
          expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled();

So that's generally how we arrived here. This could probably be improved.

Any ideas?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can get access to invoke a listener directly, that could simplify things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up PR: #88422

try {
const { start, end, tag } = queryString.parse(history.location.search);
const query = { start, end, tag, size: 20 };
const url = `/api/app_search/engines/${engineName}/analytics/queries`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not have a routePaths or similar setup in Kibana?

Copy link
Contributor Author

@cee-chen cee-chen Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't, there's some complexity there because it would need to live in the common/ folder to be shared between both our public and server folders, and so far I haven't been bothered to / seen a real need to DRY them out.

I'd definitely need to be convinced of the benefit there honestly, I find it more useful to have the explicit URL immediately available so I can check my devtools/XHR calls for debugging etc. and not have to hunt down whatever URL var the source is from (I'll admit this annoys me a decent amount in ent-search especially since our API routes are all programmatically generated so I have to go to a 404 page to even find our API routes).

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
enterpriseSearch 1076 1077 +1

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
enterpriseSearch 1.8MB 1.8MB +3.9KB

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@cee-chen cee-chen merged commit 53efccb into elastic:master Jan 14, 2021
@cee-chen cee-chen deleted the analytics-1 branch January 14, 2021 21:17
cee-chen pushed a commit that referenced this pull request Jan 14, 2021
…) (#88405)

* Set up server API routes

* Set up very basic AnalyticsLogic file

- mostly just contains API call & basic loading/error state for now - actual usable vars will be defined in a future PR

* [PR feedback] Unnecessary exports

* [PR feedback] Clean up analyticsAvailable reducer

* [PR feedback] Types order

* [PR feedback] Unnecessary API validation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature:Plugins release_note:skip Skip the PR/issue when compiling release notes v7.12.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants