19 Error Reporting
Goals
To improve the stability of Dendron and to create a better end-user experience, we would like to start monitoring errors that Dendron is encountering.
Context
Error reporting is a critical component for every large scale software project. It allows engineers to identify which problems users are encountering, to understand the criticality and impact of those problems, and to diagnose the root cause of the issue for quick resolution.
Proposal
We plan on using the sentry.io platform as a our tool of choice for error data management. The way it works is that the Dendron client will contain a Sentry error watcher; upon hitting an exception, data around that exception will get uploaded to a Sentry hosted secure database. We can then use Sentry's tooling to analyze our user crashes and errors.
Details
The types of errors we plan on capturing data for are exceptions and some non-exception induced errors.
Privacy
We take user privacy very seriously at Dendron and will not use the collected error data for any purposes other than driving bug fixes in the Dendron product. All data uploaded will be GDPR compliant.
- The data will not contain any Personally-Identifiable Information (PII) and will be completely anonymous.
- We will observe telemetry opt-out settings
- We have a data retention policy of at most 180 days.
An example error event payload can be found at the bottom of this document.
Discussion
Please leave feedback in this Github discussion
Example Error Upload
Below is an example error event that has been uploaded. Note: the local file paths in the stack trace for source maps will not appear in production; they are there in this example because I ran the test upload scenario using locally built code.
{
"event_id": "4e97f1adbc2c411cb750214c4dbc67f1",
"project": 5898219,
"release": null,
"dist": null,
"platform": "node",
"message": "",
"datetime": "2021-08-09T06:03:17.178000Z",
"tags": [
[
"browser",
"Edge 92.0.902"
],
[
"browser.name",
"Edge"
],
[
"client_os",
"Mac OS X 10.15.7"
],
[
"client_os.name",
"Mac OS X"
],
[
"device",
"Mac"
],
[
"device.family",
"Mac"
],
[
"environment",
"production"
],
[
"handled",
"yes"
],
[
"level",
"error"
],
[
"mechanism",
"generic"
],
[
"runtime",
"node v14.16.0"
],
[
"runtime.name",
"node"
],
[
"transaction",
"GET /debug-sentry"
],
[
"url",
"http://localhost/debug-sentry"
]
],
"_metrics": {
"bytes.ingested.event": 13575,
"bytes.stored.event": 20540
},
"breadcrumbs": {
"values": [
{
"timestamp": 1628488836.456,
"type": "http",
"category": "http",
"level": "info",
"data": {
"method": "POST",
"status_code": 200,
"url": "http://localhost:60285/api/workspace/initialize?"
}
},
{
"timestamp": 1628488838.64,
"type": "http",
"category": "http",
"level": "info",
"data": {
"method": "POST",
"status_code": 200,
"url": "https://api.segment.io/v1/batch"
}
},
{
"timestamp": 1628488838.644,
"type": "http",
"category": "http",
"level": "info",
"data": {
"method": "POST",
"status_code": 200,
"url": "https://api.segment.io/v1/batch"
}
},
{
"timestamp": 1628488849.764,
"type": "http",
"category": "http",
"level": "info",
"data": {
"method": "POST",
"status_code": 200,
"url": "https://api.segment.io/v1/batch"
}
},
{
"timestamp": 1628488865.322,
"type": "http",
"category": "http",
"level": "info",
"data": {
"method": "POST",
"status_code": 200,
"url": "https://api.segment.io/v1/batch"
}
},
{
"timestamp": 1628488997.154,
"type": "default",
"category": "console",
"level": "error",
"message": "My first Sentry error! [Error: My first Sentry error!\n\tat /Users/jyeung/code/dendron/dendron/packages/api-server/lib/Server.js:110:15\n\tat Layer.handle [as handle_request] (/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/layer.js:95:5)\n\tat next (/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/route.js:137:13)\n\tat Route.dispatch (/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/route.js:112:3)\n\tat Layer.handle [as handle_request] (/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/layer.js:95:5)\n\tat /Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js:281:22\n\tat Function.process_params (/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js:335:12)\n\tat next (/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js:275:10)\n\tat /Users/jyeung/code/dendron/dendron/node_modules/@sentry/tracing/dist/integrations/express.js:87:31\n\tat SendStream.error (/Users/jyeung/code/dendron/dendron/node_modules/serve-static/index.js:121:7)\n\tat SendStream.emit (events.js:315:20)\n\tat SendStream.emit (domain.js:467:12)\n\tat SendStream.error (/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js:270:17)\n\tat SendStream.onStatError (/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js:421:12)\n\tat next (/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js:735:16)\n\tat onstat (/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js:724:14)\n\tat FSReqCallback.oncomplete (fs.js:183:21)\n\tat FSReqCallback.callbackTrampoline (internal/async_hooks.js:131:14)]"
}
]
},
"contexts": {
"browser": {
"name": "Edge",
"version": "92.0.902",
"type": "browser"
},
"client_os": {
"name": "Mac OS X",
"version": "10.15.7",
"type": "os"
},
"device": {
"family": "Mac",
"model": "Mac",
"brand": "Apple",
"type": "device"
},
"runtime": {
"name": "node",
"version": "v14.16.0",
"type": "runtime"
},
"trace": {
"trace_id": "668ca236b57948d687eacf61d31dc48c",
"span_id": "b404eb7d213b7ba1",
"op": "http.server",
"status": "invalid_argument",
"data": {
"baseUrl": "",
"query": {},
"url": "/debug-sentry"
},
"tags": {
"http.status_code": "400"
},
"type": "trace"
}
},
"culprit": "GET /debug-sentry",
"environment": "production",
"exception": {
"values": [
{
"type": "Error",
"value": "My first Sentry error!",
"stacktrace": {
"frames": [
{
"function": "FSReqCallback.callbackTrampoline",
"module": "async_hooks",
"filename": "internal/async_hooks.js",
"abs_path": "internal/async_hooks.js",
"lineno": 131,
"colno": 14,
"in_app": false
},
{
"function": "FSReqCallback.oncomplete",
"module": "fs",
"filename": "fs.js",
"abs_path": "fs.js",
"lineno": 183,
"colno": 21,
"in_app": false
},
{
"function": "onstat",
"module": "send:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"lineno": 724,
"colno": 14,
"pre_context": [
" var i = 0",
" var self = this",
"",
" debug('stat \"%s\"', path)",
" fs.stat(path, function onstat (err, stat) {",
" if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {",
" // not found, check extensions"
],
"context_line": " return next(err)",
"post_context": [
" }",
" if (err) return self.onStatError(err)",
" if (stat.isDirectory()) return self.redirect(path)",
" self.emit('file', path, stat)",
" self.send(path, stat)",
" })",
""
],
"in_app": false
},
{
"function": "next",
"module": "send:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"lineno": 735,
"colno": 16,
"pre_context": [
" self.emit('file', path, stat)",
" self.send(path, stat)",
" })",
"",
" function next (err) {",
" if (self._extensions.length <= i) {",
" return err"
],
"context_line": " ? self.onStatError(err)",
"post_context": [
" : self.error(404)",
" }",
"",
" var p = path + '.' + self._extensions[i++]",
"",
" debug('stat \"%s\"', p)",
" fs.stat(p, function (err, stat) {"
],
"in_app": false
},
{
"function": "SendStream.onStatError",
"module": "send:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"lineno": 421,
"colno": 12,
"pre_context": [
" */",
"",
"SendStream.prototype.onStatError = function onStatError (error) {",
" switch (error.code) {",
" case 'ENAMETOOLONG':",
" case 'ENOENT':",
" case 'ENOTDIR':"
],
"context_line": " this.error(404, error)",
"post_context": [
" break",
" default:",
" this.error(500, error)",
" break",
" }",
"}",
""
],
"in_app": false
},
{
"function": "SendStream.error",
"module": "send:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/node_modules/send/index.js",
"lineno": 270,
"colno": 17,
"pre_context": [
" * @param {Error} [err]",
" * @private",
" */",
"",
"SendStream.prototype.error = function error (status, err) {",
" // emit if listeners instead of responding",
" if (hasListeners(this, 'error')) {"
],
"context_line": " return this.emit('error', createError(status, err, {",
"post_context": [
" expose: false",
" }))",
" }",
"",
" var res = this.res",
" var msg = statuses[status] || String(status)",
" var doc = createHtmlDocument('Error', escapeHtml(msg))"
],
"in_app": false
},
{
"function": "SendStream.emit",
"module": "domain",
"filename": "domain.js",
"abs_path": "domain.js",
"lineno": 467,
"colno": 12,
"in_app": false
},
{
"function": "SendStream.emit",
"module": "events",
"filename": "events.js",
"abs_path": "events.js",
"lineno": 315,
"colno": 20,
"in_app": false
},
{
"function": "SendStream.error",
"module": "serve-static:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/serve-static/index.js",
"lineno": 121,
"colno": 7,
"pre_context": [
" // forward errors",
" stream.on('error', function error (err) {",
" if (forwardError || !(err.statusCode < 500)) {",
" next(err)",
" return",
" }",
""
],
"context_line": " next()",
"post_context": [
" })",
"",
" // pipe",
" stream.pipe(res)",
" }",
"}",
""
],
"in_app": false
},
{
"function": "null.<anonymous>",
"module": "@sentry.tracing.dist.integrations:express",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/@sentry/tracing/dist/integrations/express.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/@sentry/tracing/dist/integrations/express.js",
"lineno": 87,
"colno": 31,
"pre_context": [
" fn.call(this, req, res, function () {",
" var args = [];",
" for (var _i = 0; _i < arguments.length; _i++) {",
" args[_i] = arguments[_i];",
" }",
" var _a;",
" (_a = span) === null || _a === void 0 ? void 0 : _a.finish();"
],
"context_line": " next.call.apply(next, tslib_1.__spread([this], args));",
"post_context": [
" });",
" };",
" }",
" case 4: {",
" return function (err, req, res, next) {",
" var _a;",
" var transaction = res.__sentry_transaction;"
],
"in_app": false
},
{
"function": "next",
"module": "express.lib.router:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js",
"lineno": 275,
"colno": 10,
"pre_context": [
" // Capture one-time layer values",
" req.params = self.mergeParams",
" ? mergeParams(layer.params, parentParams)",
" : layer.params;",
" var layerPath = layer.path;",
"",
" // this should be done for the layer"
],
"context_line": " self.process_params(layer, paramcalled, req, res, function (err) {",
"post_context": [
" if (err) {",
" return next(layerError || err);",
" }",
"",
" if (route) {",
" return layer.handle_request(req, res, next);",
" }"
],
"in_app": false
},
{
"function": "Function.process_params",
"module": "express.lib.router:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js",
"lineno": 335,
"colno": 12,
"pre_context": [
" var params = this.params;",
"",
" // captured parameters from the layer, keys and values",
" var keys = layer.keys;",
"",
" // fast track",
" if (!keys || keys.length === 0) {"
],
"context_line": " return done();",
"post_context": [
" }",
"",
" var i = 0;",
" var name;",
" var paramIndex = 0;",
" var key;",
" var paramVal;"
],
"in_app": false
},
{
"function": "null.<anonymous>",
"module": "express.lib.router:index",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/index.js",
"lineno": 281,
"colno": 22,
"pre_context": [
" // this should be done for the layer",
" self.process_params(layer, paramcalled, req, res, function (err) {",
" if (err) {",
" return next(layerError || err);",
" }",
"",
" if (route) {"
],
"context_line": " return layer.handle_request(req, res, next);",
"post_context": [
" }",
"",
" trim_prefix(layer, layerError, layerPath, path);",
" });",
" }",
"",
" function trim_prefix(layer, layerError, layerPath, path) {"
],
"in_app": false
},
{
"function": "Layer.handle [as handle_request]",
"module": "express.lib.router:layer",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/layer.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/layer.js",
"lineno": 95,
"colno": 5,
"pre_context": [
"",
" if (fn.length > 3) {",
" // not a standard request handler",
" return next();",
" }",
"",
" try {"
],
"context_line": " fn(req, res, next);",
"post_context": [
" } catch (err) {",
" next(err);",
" }",
"};",
"",
"/**",
" * Check if this route matches `path`, if so"
],
"in_app": false
},
{
"function": "Route.dispatch",
"module": "express.lib.router:route",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/route.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/route.js",
"lineno": 112,
"colno": 3,
"pre_context": [
" var method = req.method.toLowerCase();",
" if (method === 'head' && !this.methods['head']) {",
" method = 'get';",
" }",
"",
" req.route = this;",
""
],
"context_line": " next();",
"post_context": [
"",
" function next(err) {",
" // signal to exit route",
" if (err && err === 'route') {",
" return done();",
" }",
""
],
"in_app": false
},
{
"function": "next",
"module": "express.lib.router:route",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/route.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/route.js",
"lineno": 137,
"colno": 13,
"pre_context": [
" if (layer.method && layer.method !== method) {",
" return next(err);",
" }",
"",
" if (err) {",
" layer.handle_error(err, req, res, next);",
" } else {"
],
"context_line": " layer.handle_request(req, res, next);",
"post_context": [
" }",
" }",
"};",
"",
"/**",
" * Add a handler for all HTTP verbs to this route.",
" *"
],
"in_app": false
},
{
"function": "Layer.handle [as handle_request]",
"module": "express.lib.router:layer",
"filename": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/layer.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/node_modules/express/lib/router/layer.js",
"lineno": 95,
"colno": 5,
"pre_context": [
"",
" if (fn.length > 3) {",
" // not a standard request handler",
" return next();",
" }",
"",
" try {"
],
"context_line": " fn(req, res, next);",
"post_context": [
" } catch (err) {",
" next(err);",
" }",
"};",
"",
"/**",
" * Check if this route matches `path`, if so"
],
"in_app": false
},
{
"function": "null.<anonymous>",
"module": "Server",
"filename": "/Users/jyeung/code/dendron/dendron/packages/api-server/lib/Server.js",
"abs_path": "/Users/jyeung/code/dendron/dendron/packages/api-server/lib/Server.js",
"lineno": 110,
"colno": 15,
"pre_context": [
" throw Error(\"no pkg found\");",
" }",
" const version = fs_extra_1.default.readJSONSync(path_1.default.join(pkg, \"package.json\")).version;",
" return res.json({ version });",
" });",
" app.use(\"/api\", routes_1.baseRouter);",
" app.get(\"/debug-sentry\", (req, res) => {"
],
"context_line": " throw new Error(\"My first Sentry error!\");",
"post_context": [
" });",
" // The error handler must be before any other error middleware and after all controllers",
" app.use(Sentry.Handlers.errorHandler());",
" app.use((err, _req, res, _next) => {",
" console.error(err.message, err);",
" return res.status(http_status_codes_1.BAD_REQUEST).json({",
" error: err.message,"
],
"in_app": true
}
]
},
"mechanism": {
"type": "generic",
"handled": true
}
}
]
},
"fingerprint": [
"{{ default }}"
],
"grouping_config": {
"enhancements": "eJybzDRxY3J-bm5-npWRgaGlroGxrpHxBABcYgcZ",
"id": "newstyle:2019-10-29"
},
"hashes": [
"599bbfc2e3e5993211e50d0162ba5202",
"90d44251ca81b43e268c822ec5dfee44"
],
"key_id": "1744173",
"level": "error",
"location": "/Users/jyeung/code/dendron/dendron/packages/api-server/lib/Server.js",
"logger": "",
"metadata": {
"filename": "/Users/jyeung/code/dendron/dendron/packages/api-server/lib/Server.js",
"function": "null.<anonymous>",
"type": "Error",
"value": "My first Sentry error!"
},
"received": 1628488998.703633,
"request": {
"url": "http://localhost/debug-sentry",
"method": "GET",
"headers": [
[
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
],
[
"Accept-Encoding",
"gzip, deflate, br"
],
[
"Accept-Language",
"en-US,en;q=0.9"
],
[
"Connection",
"keep-alive"
],
[
"Host",
"localhost:60285"
],
[
"Sec-Ch-Ua",
"\"Chromium\";v=\"92\", \" Not A;Brand\";v=\"99\", \"Microsoft Edge\";v=\"92\""
],
[
"Sec-Ch-Ua-Mobile",
"?0"
],
[
"Sec-Fetch-Dest",
"document"
],
[
"Sec-Fetch-Mode",
"navigate"
],
[
"Sec-Fetch-Site",
"none"
],
[
"Sec-Fetch-User",
"?1"
],
[
"Upgrade-Insecure-Requests",
"1"
],
[
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"
]
]
},
"sdk": {
"name": "sentry.javascript.node",
"version": "6.11.0",
"integrations": [
"InboundFilters",
"FunctionToString",
"Console",
"OnUncaughtException",
"OnUnhandledRejection",
"LinkedErrors",
"Http",
"Express"
],
"packages": [
{
"name": "npm:@sentry/node",
"version": "6.11.0"
}
]
},
"timestamp": 1628488997.178,
"title": "Error: My first Sentry error!",
"transaction": "GET /debug-sentry",
"type": "error",
"version": "7"
}
Backlinks