Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Track actions in the UI by time #47919

Merged
merged 24 commits into from
Oct 16, 2019

Conversation

wylieconlon
Copy link
Contributor

@wylieconlon wylieconlon commented Oct 11, 2019

This PR introduces:

  • A telemetry collector that runs on a 10 second interval while the Lens app is open. Events are tracked to local storage, and if there are any events captured in 10 seconds, they are posted to the server in a batch. Events are only posted once, they are cleared after successfully sending.
  • A new server endpoint to track the usage data from the browser. Each 4-tuple is written to a saved object called lens-ui-telemetry representing the name, date, type and count of the events being tracked.
  • A daily task which aggregates all the lens-ui-telemetry documents and saved lens documents into daily buckets. This task also deletes any events older than 90 days, as we are not reporting on events past that point.
  • A stats collector which reads from the daily task, using the task state as a cache of data

The full list of event names that are introduced in this PR is:

Events that track a click:

app_filters_updated
save_failed
app_date_change
app_query_change
loaded
loaded_404
chart_switch
suggestion_confirmed
suggestion_clicked
drop_total
drop_onto_workspace
drop_onto_dimension
drop_empty
drop_non_empty
indexpattern_changed
indexpattern_filters_cleared
indexpattern_type_filter_toggled
indexpattern_existence_toggled
indexpattern_dimension_removed
indexpattern_dimension_field_changed
indexpattern_dimension_operation_<type>
indexpattern_dimension_operation_from_<type>_to_<type>
indexpattern_field_info_click
xy_change_layer_display
xy_layer_removed
xy_layer_added

Events that are triggered by the suggestion panel:

back_to_current
position_<number>_of_<number>
reload

Example of telemetry collected in its full structure:

"lens": {
  "saved_overall": {
    "bar_stacked": 1
  },
  "saved_30_days": {
    "bar_stacked": 1
  },
  "saved_90_days": {
    "bar_stacked": 1
  },
  "saved_overall_total": 1,
  "saved_30_days_total": 1,
  "saved_90_days_total": 1,
  "events_30_days": {
    "loaded": 2,
    "app_date_change": 1,
    "app_query_change": 1,
    "indexpattern_changed": 1,
    "suggestion_clicked": 1,
    "suggestion_confirmed": 1,
    "xy_layer_removed": 1
  },
  "events_90_days": {
    "loaded": 2,
    "app_date_change": 1,
    "app_query_change": 1,
    "indexpattern_changed": 1,
    "suggestion_clicked": 1,
    "suggestion_confirmed": 1,
    "xy_layer_removed": 1
  },
  "suggestion_events_30_days": {
    "position_0_of_5": 1
  },
  "suggestion_events_90_days": {
    "position_0_of_5": 1
  }
},

@elasticmachine
Copy link
Contributor

💔 Build Failed

Copy link
Contributor

@chrisdavies chrisdavies left a comment

Choose a reason for hiding this comment

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

Just a preliminary review. It looks reasonable. My comments are mostly nits. I think we discussed all of the more relevant stuff already.

x-pack/legacy/plugins/lens/public/app_plugin/app.tsx Outdated Show resolved Hide resolved

private async postToServer() {
try {
await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should track this.request or something and then just exit here, if the previous request is still running for some reason. 10 seconds should be enough, but...

x-pack/legacy/plugins/lens/server/routes/telemetry.ts Outdated Show resolved Hide resolved
x-pack/legacy/plugins/lens/server/usage/collectors.ts Outdated Show resolved Hide resolved
x-pack/legacy/plugins/lens/server/usage/collectors.ts Outdated Show resolved Hide resolved
task manager has to wait for all plugins to initialize first. It's fine to ignore it as next time around it will be
initialized (or it will throw a different type of error)
*/
if (errMessage.includes('NotInitialized')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there not a code or something systematic that we could use to do this check?

[TELEMETRY_TASK_TYPE]: {
title: 'Lens telemetry fetch task',
type: TELEMETRY_TASK_TYPE,
timeout: '1m',
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should have a longer timeout, just in case

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@wylieconlon wylieconlon marked this pull request as ready for review October 14, 2019 22:47
@wylieconlon wylieconlon requested a review from a team as a code owner October 14, 2019 22:47
@wylieconlon wylieconlon requested a review from a team October 14, 2019 22:47
@wylieconlon wylieconlon added release_note:skip Skip the PR/issue when compiling release notes Team:Visualizations Visualization editors, elastic-charts and infrastructure v7.5.0 v8.0.0 labels Oct 14, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-app (Team:KibanaApp)

@wylieconlon wylieconlon requested a review from Bamieh October 14, 2019 22:47
Copy link
Contributor

@chrisdavies chrisdavies left a comment

Choose a reason for hiding this comment

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

LGTM w/ tweaks.

buckets: SubAggregationMap extends {
filters: { filters: Record<string, unknown> };
}
? {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm unfamiliar with this TypeScript signature. TIL

) {
trackUiEvent('app_date_change');
} else {
trackUiEvent('app_query_change');
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this actually interesting? I'm not sure it's worth collecting, as I expect it to be just super noisy.

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 could combine this with filter changes, but I do think it's interesting

const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => {
trackUiEvent('loaded');
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this worth tracking? Maybe. 404 certainly is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we don't track some kind of loaded event it won't be possible to create any kind of funnel, because these metrics are in a window and no other metrics are

if (lastSelectedSuggestion === index) {
rollbackToCurrentVisualization();
} else {
trackSuggestionEvent(`index_${index}_of_${suggestions.length}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe suggestion_${index}_of_${suggestions.length}? I had to stare at this for a bit to realize that index was not an Elasticsearch index name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's why it's a different function call- Raya wanted to track the position of suggestions. I'll rename to position

metrics.aggregations!.daily.buckets.forEach(daily => {
const byType: Record<string, number> = byDateByType[daily.key] || {};
daily.groups.buckets.regularEvents.names.buckets.forEach(bucket => {
byType[bucket.key] = bucket.sums.value || 0 + (byType[daily.key] || 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

It's hard for me to follow this, so am just commenting here for you to double-check that the use of || is really what you want here, same for below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is

const byDateByType: Record<string, Record<string, number>> = {};
const suggestionsByDate: Record<string, Record<string, number>> = {};

metrics.aggregations!.daily.buckets.forEach(daily => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure this can never be undefined? Same for the deeper properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As long as the .kibana index exists, the aggregations key will be returned. And if not, this entire task will fail but that's also handled

query: {
bool: {
filter: [
{ term: { type: 'lens-ui-telemetry' } },
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should pull 'lens-ui-telemetry' out into a const?

};
}

function getNextMidnight() {
Copy link
Contributor

Choose a reason for hiding this comment

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

You're using moment everywhere else, why not here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was copied

}

export interface LensVisualizationUsage {
saved_overall: Record<string, number>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be sure this can't explode in size, we should double-check that our generated event names have a realistically small potential set of values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These aren't generated names, they're all strings in code

Copy link

@AlonaNadler AlonaNadler left a comment

Choose a reason for hiding this comment

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

Great work @wylieconlon I can't wait to start using it !!

  • how does the suggestions fields work e.g. "index_0_of_5": 1 ?
  • Using workspace_drop_non_empty: 1 I want to know how common users drag more than 1 fields, does it make sense to do AVG(workspace_drop_non_empty/workspace_drop_empty)?
  • suggestion_confirmed: 1 is it suggestion selected & saved?
  • What is the definition of loaded?
  • What's the definition of app_query_change is it a change from the configuration panel?

`

@wylieconlon
Copy link
Contributor Author

@AlonaNadler

how does the suggestions fields work e.g. "index_0_of_5": 1 ?

Renamed to position_0_of_5. All the suggestion events are also added to something like suggestion_clicked in the events list

Using workspace_drop_non_empty: 1 I want to know how common users drag more than 1 fields, does it make sense to do AVG(workspace_drop_non_empty/workspace_drop_empty)?

No, that would not make sense. I've changed it so that there is now a number that tracks all drops regardless of their destination, and then breaks that out into:

  • All drops that were empty, vs all that were non-empty
  • All drops onto workspace vs all onto a dimension

Both of those should add up to 100%

suggestion_confirmed: 1 is it suggestion selected & saved?

This is number of clicks on the "Reload" button

What is the definition of loaded?

This tracks any page load, including ones that happen because the user saved. I can change this definition.

What's the definition of app_query_change is it a change from the configuration panel?

For example, a KQL filter was typed into the query bar.

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Copy link
Contributor

@chrisdavies chrisdavies left a comment

Choose a reason for hiding this comment

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

LGTM w/ tweaks. I think you need to correct the operator precedence stuff I commented on in this review and my previous one. But other than that, I think this is fine.

@@ -121,7 +122,8 @@ export function DragDrop(props: Props) {
setState({ ...state, isActive: false });
setDragging(undefined);

if (onDrop) {
if (onDrop && droppable) {
trackUiEvent('drop_total');
Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably fine, but if we ever want to make this drag / drop thing more general, this would be something that requires yanking out.

@@ -137,6 +138,10 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan
field: droppedItem.field,
});

trackUiEvent('drop_onto_dimension');
const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length);
trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty');
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine, but also, do we need three different events? drop_onto_dimension would be a sum of drop_non_empty and drop_empty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's actually five events:

drop_total is every drop

Two pairs:

drop_onto_workspace + drop_onto_dimension = drop_total
drop_empty + drop_non_empty = drop_total

}

private async postToServer() {
this.readFromStorage();
Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably fine, but it does introduce a disk read for a whole bunch of user-driven events. I wonder if this'll have any impact on performance on low-end devices... Probably not.

Anyway, an alternative would be to track all the stuff in memory, but when you flush it to storage, read and merge then, and reset your in-memory 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.

There are probably some alternatives here, but this is the simplest implementation of it


function addEvents(prevEvents: Record<string, number>, newEvents: Record<string, number>) {
Object.keys(newEvents).forEach(key => {
prevEvents[key] = prevEvents[key] || 0 + newEvents[key];
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure why lint won't let you put parenthesis there, but I think you should, and should add a lint exception comment.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

Copy link
Member

@Bamieh Bamieh left a comment

Choose a reason for hiding this comment

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

This collection method almost duplicates @kbn/analytics code just for providing a new collection method. This UI metric can be easily baked into the @kbn/analytics library by introducing it as a new METRIC_TYPE. Doing so it makes it easier to manage telemetry, refactor code and have others utilize these metric types.

I have opened an issue for this as a follow up to this PR: #48352

While it is nice to have this tracking as soon as possible for lens. Ideally implementing this would be approached differently; I've opened an issue so we can revisit the use of this metric type so we can learn its usefulness for the consumers of this usage data and then introduce it as METRIC_TYPE in @kbn/analytics for others to be able to use it as well.

I have opened an issue for this as a follow up to this PR: #48353

While not completely thrilled about introducing new telemetry code floating outside our telemetry packages (ui_metric / telemetry / and @kbn/analytics); I believe we can merge this and revisit these concerns afterwards.

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@wylieconlon
Copy link
Contributor Author

@Bamieh You are right that it is heavily based on the existing code, and I am looking forward to integrating some of these changes for the rest of Kibana to use in releases.

@wylieconlon wylieconlon merged commit fba41f6 into elastic:master Oct 16, 2019
@wylieconlon wylieconlon deleted the lens/click-telemetry branch October 16, 2019 03:01
wylieconlon pushed a commit to wylieconlon/kibana that referenced this pull request Oct 16, 2019
* [Lens] Track actions in the UI by time

* Switch collector to use task data

* Report summarized version of task data when requested

* Track a more complete set of metrics

* Collect suggestion events separately

* Pre-aggregate by date in localStorage

* Add integration tests

* Fix test linter

* Fix telemetry naming and run at midnight instead of every minute

* Improve cleanup at app level

* Fix lint errors

* Remove unused mock

* Fix tests

* Fix types

* Update event names and fix local tracking

* Respond to review comments

* Fix task time

* Fix test
wylieconlon pushed a commit that referenced this pull request Oct 16, 2019
* [Lens] Track actions in the UI by time

* Switch collector to use task data

* Report summarized version of task data when requested

* Track a more complete set of metrics

* Collect suggestion events separately

* Pre-aggregate by date in localStorage

* Add integration tests

* Fix test linter

* Fix telemetry naming and run at midnight instead of every minute

* Improve cleanup at app level

* Fix lint errors

* Remove unused mock

* Fix tests

* Fix types

* Update event names and fix local tracking

* Respond to review comments

* Fix task time

* Fix test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Lens release_note:skip Skip the PR/issue when compiling release notes Team:Visualizations Visualization editors, elastic-charts and infrastructure v7.5.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants