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

Distributed error reporting: Capture more information about the error and its environment #12382

Open
wants to merge 15 commits into
base: distributed-error-reporting
Choose a base branch
from

Conversation

thesujai
Copy link
Contributor

@thesujai thesujai commented Jul 1, 2024

Summary

  1. Changes the model ErrorReports by renaming some fields like error_from to category and more.
  2. Adds new JSON fields for backend and frontend so we can store more contexts of error into them
  3. Update the remaining part of the code with the model changes, by update in backend error handling and frontend error handling mechanism

References

Closes #12376

Reviewer guidance

Cannot find a way yo get the node version in frontend


Testing checklist

  • Contributor has fully tested the PR manually
  • If there are any front-end changes, before/after screenshots are included
  • Critical user journeys are covered by Gherkin stories
  • Critical and brittle code paths are covered by unit tests

PR process

  • PR has the correct target branch and milestone
  • PR has 'needs review' or 'work-in-progress' label
  • If PR is ready for review, a reviewer has been added. (Don't use 'Assignees')
  • If this is an important user-facing change, PR or related issue has a 'changelog' label
  • If this includes an internal dependency change, a link to the diff is provided

Reviewer checklist

  • Automated test coverage is satisfactory
  • PR is fully functional
  • PR has been tested for accessibility regressions
  • External dependency files were updated if necessary (yarn and pip)
  • Documentation is updated
  • Contributor is in AUTHORS.md

@github-actions github-actions bot added DEV: backend Python, databases, networking, filesystem... DEV: frontend SIZE: large labels Jul 1, 2024
@thesujai thesujai changed the title Distributed error reporting task7 Distributed error reporting: Capture more information about the error and its environment Jul 1, 2024
@akolson akolson force-pushed the distributed-error-reporting-task7 branch from 6459176 to da7f8f0 Compare July 1, 2024 21:20
@akolson akolson requested review from rtibbles and akolson July 2, 2024 16:51
component: this.vm.$options.name || this.vm.$options._componentTag || 'Unknown Component',
browser: this.getBrowserInfo(),
device: this.getDeviceInfo(),
},
Copy link
Member

Choose a reason for hiding this comment

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

context can be movet to ErrorReport too. We can then do something like the below;

getErrorReport() {
    const context = this.getContext();
    return {
      ...
      context: {
        component: this.vm.$options.name || this.vm.$options._componentTag || 'Unknown Component',
        ...context,
      },
    };
  }

Copy link
Member

Choose a reason for hiding this comment

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

Could even not even need to define getErrorReport in this class, instead can just define it in the parent class once, and then here we do:

getContext() {
   const context = super.getContext();
   return {
      ...context,
      component <etc>...
   };
}

context: {
browser: this.getBrowserInfo(),
device: this.getDeviceInfo(),
},
Copy link
Member

Choose a reason for hiding this comment

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

Same here https://github.com/learningequality/kolibri/pull/12382/files#r1662911798. Though we would just assign the context

context: this.getContext()

context: {
browser: this.getBrowserInfo(),
device: this.getDeviceInfo(),
},
Copy link
Member

Choose a reason for hiding this comment

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

"device": {
"type": "object",
"properties": {
"type": {"type": "string"}, # "desktop", "tablet", "mobile"
Copy link
Member

@akolson akolson Jul 2, 2024

Choose a reason for hiding this comment

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

With json schema we can ensure the device is from a predefined list of device types

"url": {"type": "string", "optional": True},
"method": {"type": "string", "optional": True},
"headers": {"type": "object", "optional": True},
"body": {"type": "string", "optional": True},
Copy link
Member

Choose a reason for hiding this comment

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

I think properties are optional by default so there is not need to explicitly set them as optional. So you might as well do away with the optional flag?

Copy link
Member

Choose a reason for hiding this comment

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

Also, we can use the required property that specifies the required fields instead where necessary

Copy link
Member

Choose a reason for hiding this comment

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

(There are other sections in the schema with the same)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@akolson I too had a doubt about this. But this comment suggested otherwise:

# '"optional":True' is obsolete but needed while we keep using an

Copy link
Member

Choose a reason for hiding this comment

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

Since the use of the optional property is specific to a particular version of jsonschema, we'll carry on with using it, but create a follow up issue to to update all jsonschemas as python 2.7 support has been dropped (the main reason the current version of jsonschema is used)

"platform": {
"type": "string",
"optional": True,
}, # "windows", "mac", "linux", "android", "ios"
Copy link
Member

Choose a reason for hiding this comment

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

With json schema we can ensure the platform is from a predefined list of platforms

@@ -6,13 +6,35 @@ class ErrorReport {
getErrorReport() {
throw new Error('getErrorReport() method must be implemented.');
}
getDeviceInfo() {
return {
type: /Mobi|Android/i.test(navigator.userAgent) ? 'Mobile' : 'Desktop',
Copy link
Member

Choose a reason for hiding this comment

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

This will return the wrong type if kolibri is used on a tablet for example.

Copy link
Member

Choose a reason for hiding this comment

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

(Just noting that this is quite an unreliable way to check for device type)

Copy link
Member

Choose a reason for hiding this comment

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

Yes, definitely just relying on the values already defined in browserInfo.js seems like the right way forward here, no need to write new code when we already have robust parsing of the userAgent.

getDeviceInfo() {
return {
type: /Mobi|Android/i.test(navigator.userAgent) ? 'Mobile' : 'Desktop',
platform: navigator.platform,
Copy link
Member

Choose a reason for hiding this comment

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

We can use os in browserInfo.js

getErrorReport() {
return {
error_message: this.e.message,
traceback: this.e.stack,
context: {
component: this.vm.$options.name || this.vm.$options._componentTag || 'Unknown Component',
browser: this.getBrowserInfo(),
Copy link
Member

Choose a reason for hiding this comment

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

We can use browser in browserInfo.js



def get_python_version():
return version.split()[0]
Copy link
Member

Choose a reason for hiding this comment

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

I think this would only pick the first version of the tuple. See here

@@ -47,25 +54,55 @@ def allow_migrate(self, db, app_label, model_name=None, **hints):


class ErrorReports(models.Model):
Copy link
Member

Choose a reason for hiding this comment

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

Also noting that

info["installer"] = installation_type()
is a very important piece of information to store.

Copy link
Member

Choose a reason for hiding this comment

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

Similarly, we should store is_touch_device to clearly distinguish between touch and non touch devices

errors_json = serialize_error_reports_to_json_response(errors)

requests.post(
join_url(server, "/api/v1/errors/"),
Copy link
Member

Choose a reason for hiding this comment

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

Let's change the resource to /api/v1/errors/report/ for clarity.

@akolson akolson self-requested a review July 12, 2024 12:07
device.type = device.type || 'unknown';
device.model = device.model || 'unknown';
device.vendor = device.vendor || 'unknown';
export const deviceWithTouch = {
Copy link
Member

@akolson akolson Jul 12, 2024

Choose a reason for hiding this comment

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

I think this would be superfluous!
I think it would be more straight forward to just export device info only. I dont see the need to bundle it up with the device info.

I think the below would suffice

export const device = info.device;

Copy link
Member

Choose a reason for hiding this comment

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

Also deviceWithTouch could be misinterpreted to mean that it represents an object with touch capabilities which is not really the case -- isTouchDevice is a runtime value.

// this is better than undefined
device.type = device.type || 'unknown';
device.model = device.model || 'unknown';
device.vendor = device.vendor || 'unknown';
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure there any value add of doing this!

@@ -1,3 +1,5 @@
import { browser, os, deviceWithTouch } from './browserInfo';
Copy link
Member

@akolson akolson Jul 12, 2024

Choose a reason for hiding this comment

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

Lets import device and isTouchDevice instead of deviceWithTouch here. See https://github.com/learningequality/kolibri/pull/12382/files#r1675763461

browser: browser,
os: os,
device: {
...deviceWithTouch,
Copy link
Member

Choose a reason for hiding this comment

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

instead of deviceWithTouch, we would have

device: {
    ...device,
    isTouchDevice,
    screen: {
        ...
    }
}

Comment on lines +7 to +10
"name": {"type": "string", "optional": True},
"major": {"type": "string", "optional": True},
"minor": {"type": "string", "optional": True},
"patch": {"type": "string", "optional": True},
Copy link
Member

Choose a reason for hiding this comment

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

These can de defined as a property and referenced instead...

Comment on lines 17 to 20
"name": {"type": "string", "optional": True},
"major": {"type": "string", "optional": True},
"minor": {"type": "string", "optional": True},
"patch": {"type": "string", "optional": True},
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

For ecample it would mean adding definitions

....
"definitions": {
    "versionInfo": {
      "type": "object",
      "properties": {
        "name": {"type": "string", "optional": True},
        "major": {"type": "string", "optional": True},
        "minor": {"type": "string", "optional": True},
        "patch": {"type": "string", "optional": True},
      }
    }
  }

and then use it as below;

"os":  {
    "$ref": "#/definitions/versionInfo",
},
...
"browser": {
    "$ref": "#/definitions/versionInfo",
}

Copy link
Member

@akolson akolson left a comment

Choose a reason for hiding this comment

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

Hi @thesujai! Great work on the changes, The pr is now taking shape! I left a few comments that would be good to implement. Thanks

@@ -64,6 +64,13 @@ export const os = {
patch: osVersion[2],
};

// Device info
export const device = {
type: info.device.type || 'desktop',
Copy link
Member

Choose a reason for hiding this comment

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

Why are we defaulting to 'desktop'?

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 ua-parser library gives undefined for desktop. As according to them desktop is not an device type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DEV: backend Python, databases, networking, filesystem... DEV: frontend SIZE: large
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants