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

[WIP] Move Google Analytics and Sentry to hooks that apps can call #196

Merged
merged 9 commits into from
Sep 11, 2018

Conversation

dmethvin-gov
Copy link
Contributor

Fixes #134

Putting this up for discussion now since there are some thorny questions about back-compat with the existing vets.gov--see comments.

The basic idea is to move the GA and error reporting to the app rather than embed it in the library. This gives the app writer the opportunity to decide what should be reported and which one of the dozens of event reporter services they should use. I've put the hook in formConfig.clientTelemetry but better names are welcome. The hook function does not need to be passed the formConfig because the app writer's hook function can always get it directly from their own project or use formConfig.clientTelemetry.bind(formConfig) if they prefer.

Once design issues are resolved this PR still needs:

  • Docs on the feature as finally implemented
  • Unit tests

And separately in us-forms-system-starter-app

  • Skeletal implementation, using console.error or similar perhaps?

package.json Outdated
@@ -28,19 +28,19 @@
},
"homepage": "https://github.com/usds/us-forms-system#readme",
"devDependencies": {
Copy link
Contributor Author

@dmethvin-gov dmethvin-gov Jul 30, 2018

Choose a reason for hiding this comment

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

An npm update slipped in here, I can back it out if needed. The only related change here is that raven-js was removed. If the app wanted it they would name it directly as a dependency.

});
const telemetry = (category, error) => {
if (formConfig.clientTelemetry) {
formConfig.clientTelemetry(category, error);
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 clientTelemetry call can do both a Sentry and GA.

}
captureError(error, errorType);
dispatch(setSubmission('status', errorType, error.extra));
telemetry('error', error);
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 are obviously errors specific to vets.gov, so I pulled this out. If vets.gov needs site-specific messages they could be sent by the hook function. (Essentially, move the deleted code to the hook function I think,)

@@ -241,7 +225,6 @@ export function uploadFile(file, uiOptions, onProgress, onChange, onError) {
name: file.name,
errorMessage
});
Raven.captureMessage(`vets_upload_error: ${req.statusText}`);
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 code doesn't currently have access to formConfig as an argument, but in addition the error is vets.gov-specific so it seems like this needs to be isolated some way. Also, we don't have file uploads implemented anyway so this code doesn't run and can't be tested.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just move file upload out of our library until we can build it in a generally usable way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably. It's super useful functionality but we need it to be generalized so it can work with other servers.

@annekainicUSDS
Copy link
Contributor

This is interesting so far, and definitely a good idea to find a way to pull these events out of the library. What I don't understand right now is how the information being logged to clientTelemetry gets sent back to the app?

@dmethvin-gov
Copy link
Contributor Author

That would be the job of the app. Per that last checkbox I figured we could add a sample hook to the starter app that just logged to the console. I don't think we should actually try to put real telemetry in the sample app, it requires the app-writer's Google Analytics or error reporting provider's number.

Looking back through the library, we don't really log a lot here. I think it would be interesting to fire the callback each time the person navigates to a page so it would be possible to get form-abandon stats. This isn't data that vets.gov is expecting so it would have to be filtered out in their app-specific hook when they upgraded to this version.

@dmethvin-gov
Copy link
Contributor Author

I'm going to wait until #209 lands because this one touches nearby areas and will need to be fixed.

@@ -242,7 +229,8 @@ export function uploadFile(file, uiOptions, onProgress, onChange, onError) {
name: file.name,
errorMessage
});
Raven.captureMessage(`vets_upload_error: ${req.statusText}`);
// Commenting until this is removed by a PR for #211
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll be pulling out the vets-specific file upload code when #211 is fixed but I didn't want to start unraveling it here.

};
const recordEvent = formConfig.recordEvent ?
formConfig.recordEvent.bind(formConfig) :
console.log.bind(console); // eslint-disable-line no-console
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 seemed bad to be totally silent when some of these things happened so I figured we could put them on the console. If someone is upset because the console has been soiled and they don't want the messages, they can define a function that does nothing. 😨

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel a little weary of publishing a bunch of stuff to the console, but I don't really know what's best practice here. It seems like warnings that are useful for development make sense to be logged to the console, but I'm not aware of any apps that log user-created events to the console. Are there risks with this?

}
});
const recordEvent = formConfig.recordEvent ?
formConfig.recordEvent.bind(formConfig) :
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 this is a duplicate of the code in SubmitController. I was thinking that we might not want to log some messages that I want to add in the future (e.g. { event: 'user-changed-pages', from: 2, to: 3 }) so it seemed bad to overwrite formConfig.recordEvent with a global default.

@@ -578,13 +578,3 @@ export function omitRequired(schema) {

return newSchema;
}

/**
* Helper function for reporting events to Google Analytics. An alias for window.dataLayer.push.
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 an app wants to use GA they'd do that in their app.

@@ -463,8 +463,7 @@ export function createInitialState(formConfig) {
reviewPageView: {
openChapters: [],
viewedPages: new Set()
},
trackingPrefix: formConfig.trackingPrefix
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need trackingPrefix anymore since that data would be held by the app and depend on what service they used.

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 confused how this part would work. It seems like they may still want a way to differentiate between events of the same name that happen on different forms, so it seems like this would still be needed in our code when we log events?

@dmethvin-gov
Copy link
Contributor Author

I think this is ready for another review pass.

This version has docs for the feature, I was unsure of where to put them.

Copy link
Contributor

@bernars-usa bernars-usa left a comment

Choose a reason for hiding this comment

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

Just minor comments from me!


The `recordEvent` function receives a single object that can contain different information depending on the kind of event being reported. Events reported by the library always have a `event` property that is a string describing the event. The events currently supported by the library are:
* **validation-failed-on-submit**: The user completed the form and tried to submit it, but there were still validation errors. This is most likely an error in the form definition or one of the React components, since the form should already be fully validated by the time this point is reached.
* **form-submit-pending**: The user has pressed the submit button, the form validated, and the form has been sent to the server. This is informational only and does not represent an error.
Copy link
Contributor

Choose a reason for hiding this comment

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

The user has pressed the submit button, the form validated, and the form has been sent to the server. This is informational only and does not represent an error.

Maybe:

"An informational message showing that the user has pressed the submit button, and that the form has validated and been sent to the server."

* **validation-failed-on-submit**: The user completed the form and tried to submit it, but there were still validation errors. This is most likely an error in the form definition or one of the React components, since the form should already be fully validated by the time this point is reached.
* **form-submit-pending**: The user has pressed the submit button, the form validated, and the form has been sent to the server. This is informational only and does not represent an error.
* **form-submit-successful**: The server returned a status indicating it has accepted the form.
* **form-submit-error**: The form was submitted but some problem occurred that prevented it from being accepted by the server. The object contains an `error` and `errorType` with more information about the nature of the error.
Copy link
Contributor

Choose a reason for hiding this comment

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

The form was submitted but some problem occurred that prevented it from being accepted by the server. The object contains an error and errorType with more information about the nature of the error.

How about:

"The form was submitted, but the server didn't accept the submission. The object contains an error and errorType with more information about the nature of the error."

If you provide a function for `formConfig.recordEvent`, the library calls that function when notable events occur. If you do not provide a function, the library logs these events onto the browser console.

The `recordEvent` function receives a single object that can contain different information depending on the kind of event being reported. Events reported by the library always have a `event` property that is a string describing the event. The events currently supported by the library are:
* **validation-failed-on-submit**: The user completed the form and tried to submit it, but there were still validation errors. This is most likely an error in the form definition or one of the React components, since the form should already be fully validated by the time this point is reached.
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 most likely an error in the form definition or one of the React components, since the form should already be fully validated by the time this point is reached.

Can we get away with:

"This is likely an error in the form definition or a React component, since the form should be completely validated at this point."

Copy link
Contributor

@annekainicUSDS annekainicUSDS left a comment

Choose a reason for hiding this comment

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

Overall the direction makes sense to me! Some questions.

recordEvent({
event: `${trackingPrefix}-submission-successful`,
});
recordEvent({ event: 'form-submit-successful' });
Copy link
Contributor

Choose a reason for hiding this comment

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

The tracking prefix that existed before in the event name was useful for Vets.gov because there are so many forms that they need to be able to know which one specifically was successful. I assume this could be the case for other users as well. Do you think we can account for this the way we did before, by including a key in the formConfig for trackingPrefix? Or is there a better way? Maybe allowing someone to customize the message that gets passed for various events?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The function that gets called is provided by the app (in that case, the vets.gov website) so they can make whatever changes they'd like for each event, and it can be customized per event. We don't need to incorporate any values in the formConfig since they can add whatever they want in their function. One thing that I did which may be worth reconsidering is I used .bind() to bind the function to the formConfig, but on second thought it makes more sense to have the creator of form.js do that if that's what they want. It's possible they might want to pass in a bound "EventReportingClass" method that encapsulates whatever data they need including trackingPrefix.

@@ -177,7 +164,7 @@ export function submitForm(formConfig, form) {
} else if (errorMessage.startsWith('vets_server_error')) {
errorType = 'serverError';
}
captureError(error, errorType);
recordEvent({ event: 'form-submit-error', error, errorType });
Copy link
Contributor

Choose a reason for hiding this comment

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

So is this new event, form-submit-error replacing the submission-failed event we previously had? It makes me wonder about current users of the library that the names of the logged errors will be changing, and if that will create a downstream problem for them in tracking issues. I wonder if we should enable someone to customize the names of the events that get logged.

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 changes have to occur in a major version release so they're breaking changes regardless. Most likely the only client using this logic at the moment is vets.gov because of the very specific configuration and messages (e.g., vets_server_error above). In their recordEvent setup they can just map form-submit-error to submission-failed if they want to keep the same name. I just wanted to make the names consistent and since these three were about the form submit process I used the same prefix for their names.

formConfig.recordEvent.bind(formConfig) :
console.log.bind(console); // eslint-disable-line no-console

recordEvent({ event: 'validation-failed-on-submit', errors });
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes me wonder about where these events should be logged. Below we're also dispatching a redux action for the submission status, so there are 2 places here where information is being communicated out about the same event. Should we consolidate this somehow? Should the events be logged from the updated state? I'm not sure, just opening it as a possible discussion point :)

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 suspected that duplication was intentional so I kept it. The event recording is as independent as possible to the app logic to avoid being affected by app bugs.

@@ -463,8 +463,7 @@ export function createInitialState(formConfig) {
reviewPageView: {
openChapters: [],
viewedPages: new Set()
},
trackingPrefix: formConfig.trackingPrefix
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 confused how this part would work. It seems like they may still want a way to differentiate between events of the same name that happen on different forms, so it seems like this would still be needed in our code when we log events?

};
const recordEvent = formConfig.recordEvent ?
formConfig.recordEvent.bind(formConfig) :
console.log.bind(console); // eslint-disable-line no-console
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel a little weary of publishing a bunch of stuff to the console, but I don't really know what's best practice here. It seems like warnings that are useful for development make sense to be logged to the console, but I'm not aware of any apps that log user-created events to the console. Are there risks with this?

@dmethvin-gov
Copy link
Contributor Author

GitHub is being bizarre today, I can't reply to two (but only two) of Anne's comments so I'll do it here.

I'm confused how this part would work. It seems like they may still want a way to differentiate between events of the same name that happen on different forms, so it seems like this would still be needed in our code when we log events?

The person setting up the form.js can configure whatever they want including unique strings and account numbers to prefix or suffix (or even rename) the event names that we pass back. The starter app can have a stub showing how that's done.

I feel a little weary of publishing a bunch of stuff to the console, but I don't really know what's best practice here. It seems like warnings that are useful for development make sense to be logged to the console, but I'm not aware of any apps that log user-created events to the console. Are there risks with this?

I don't really like spewing on the console either, but except for the form-submit-successful these represent some sort of bug in the program so it seemed worse to be silent about it by default, especially on the production forms. I thought about having some sort of "priority" or "severity" that would separate info/warning/error but wondered if that was overkill. I haven't seen what happens on the screen when the validation-failed-on-submit case occurs but it seems like the user would have no clue at all and no action occurred when they pressed the button.

@dmethvin-gov
Copy link
Contributor Author

Oh @annekainicUSDS maybe one option would be to make formConfig.recordEvent mandatory? That way the app has to do something with the data, even if just to unwisely throw it away. In that case we'd eliminate the console.log on our side.

Copy link
Contributor

@annekainicUSDS annekainicUSDS left a comment

Choose a reason for hiding this comment

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

In general looks good to me! I think there might still be some open questions (logging on the console? making recordEvent mandatory?), but perhaps we get feedback on that once someone (probably Vets.gov) actually starts using this and lets us know what they think.


### Usage guidelines

Web applications can fail for many reasons, including bad Internet connections, outdated browsers, and misbehaved browser extensions. Even if you test thoroughly, users may experience frustrating errors that they do not report. We highly recommend that you use an error and event reporting service to track the use of your forms. Examples of services that could be used are Google Analytics, Errorception, Sentry, Airbrake, and Raygun.
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about including a code sample to show what this function might look like? Something like:

"Here's an example of what the function passed to formConfig.recordEvent might look like if you were using Google Analytics to track events:

formConfig = {
  ...
  recordEvent: (event) => {
    return window.dataLayer && window.dataLayer.push(event);
  },
  ...

"

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also an example of how they would change the name of the event they log on their side?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Document the ability to track form stats and errors via Google Analytics
3 participants