diff --git a/CHANGELOG.md b/CHANGELOG.md index aab562e37..2158b0fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ [1277](https://github.com/nextcloud/cookbook/pull/1277) - Prepare the GitHub action scripts to be compatible with the upcoming version split in version 0.10.0 [#1285](https://github.com/nextcloud/cookbook/pull/1285) @christianlupus +- Add logging to diagnose bugs in production + [#1283](https://github.com/nextcloud/cookbook/pull/1283) @MarcelRobitaille ### Documentation - Fix bad writing diff --git a/docs/dev/frontend/logging.md b/docs/dev/frontend/logging.md new file mode 100644 index 000000000..fdb3eebde --- /dev/null +++ b/docs/dev/frontend/logging.md @@ -0,0 +1,21 @@ +# Debugging in production through logging + +Sometimes, it is necessary to debug problems in production +where it is not possible to use a debugger or other developer tools to find the +source of the problem. +For example, if a user reports an issue that cannot be reproduced locally, +you can instruct them to enable logging to narrow down the source of the issue. + +To enable logging, a user only has to run the following before loading the app/refreshing the page: +```js +localStorage.setItem('COOKBOOK_LOGGING_ENABLED', 'true') +``` + +This will automatically be reset after 30 minutes so verbose logging is not enabled permanently for the user. + +The log level is also configurable. For example: +```js +localStorage.setItem('COOKBOOK_LOGGING_LEVEL', 'debug') +``` + +The documentation for the logging library used is available [here](https://www.npmjs.com/package/vuejs-logger). diff --git a/docs/dev/index.md b/docs/dev/index.md index a7494ddee..d608d0301 100644 --- a/docs/dev/index.md +++ b/docs/dev/index.md @@ -18,6 +18,7 @@ If you want to **help contributing**, please see the [page about contributing](c - [Webpack `BundleAnalyzer` Plugin](frontend/webpack-bundle-analyzer) - [Hot reload capability for faster frontend development](frontend/hot-reload) +- [Debugging in production through logging](frontend/logging) ## Backend diff --git a/package-lock.json b/package-lock.json index 4274cb8c8..37eb4387c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "vue-modal-dialogs": "3.0.0", "vue-router": "^3.1.6", "vue-showdown": "^2.4.1", + "vuejs-logger": "1.5.5", "vuex": "^3.1.3" }, "devDependencies": { @@ -3572,9 +3573,7 @@ "node_modules/es6-object-assign": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", - "dev": true, - "peer": true + "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" }, "node_modules/escalade": { "version": "3.1.1", @@ -9942,6 +9941,20 @@ "vue": "^2.5.0" } }, + "node_modules/vuejs-logger": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/vuejs-logger/-/vuejs-logger-1.5.5.tgz", + "integrity": "sha512-wESz1F4KWk98YANEDg2yeS+fpwk2WrR41ZslLfZgTD+EYFm/7VMMUjRThhHT8CCOLOCQdsS4Ge2C9bIs68v8Ww==", + "dependencies": { + "es6-object-assign": "1.1.0", + "vue": "2.6.11" + } + }, + "node_modules/vuejs-logger/node_modules/vue": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz", + "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==" + }, "node_modules/vuex": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz", @@ -13587,9 +13600,7 @@ "es6-object-assign": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", - "dev": true, - "peer": true + "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" }, "escalade": { "version": "3.1.1", @@ -18387,6 +18398,22 @@ "date-format-parse": "^0.2.7" } }, + "vuejs-logger": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/vuejs-logger/-/vuejs-logger-1.5.5.tgz", + "integrity": "sha512-wESz1F4KWk98YANEDg2yeS+fpwk2WrR41ZslLfZgTD+EYFm/7VMMUjRThhHT8CCOLOCQdsS4Ge2C9bIs68v8Ww==", + "requires": { + "es6-object-assign": "1.1.0", + "vue": "2.6.11" + }, + "dependencies": { + "vue": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz", + "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==" + } + } + }, "vuex": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz", diff --git a/package.json b/package.json index 2355f96d4..f89182bf0 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "vue-modal-dialogs": "3.0.0", "vue-router": "^3.1.6", "vue-showdown": "^2.4.1", + "vuejs-logger": "1.5.5", "vuex": "^3.1.3" }, "devDependencies": { diff --git a/src/components/AppIndex.vue b/src/components/AppIndex.vue index 310295b09..9315439e3 100644 --- a/src/components/AppIndex.vue +++ b/src/components/AppIndex.vue @@ -38,6 +38,7 @@ export default { }, }, mounted() { + this.$log.info("AppIndex mounted") this.loadAll() }, methods: { diff --git a/src/components/AppMain.vue b/src/components/AppMain.vue index 2a1356b86..b9f649065 100644 --- a/src/components/AppMain.vue +++ b/src/components/AppMain.vue @@ -50,6 +50,7 @@ export default { */ }, mounted() { + this.$log.info("AppMain mounted") subscribe("navigation-toggled", this.updateAppNavigationOpen) }, unmounted() { diff --git a/src/components/AppNavi.vue b/src/components/AppNavi.vue index 4037744f3..2ec73c4af 100644 --- a/src/components/AppNavi.vue +++ b/src/components/AppNavi.vue @@ -149,6 +149,7 @@ export default { }, computed: { totalRecipeCount() { + this.$log.debug("Calling totalRecipeCount") let total = this.uncatRecipes for (let i = 0; i < this.categories.length; i++) { total += this.categories[i].recipeCount @@ -168,11 +169,13 @@ export default { // Register a method hook for navigation refreshing refreshRequired(newVal, oldVal) { if (newVal !== oldVal && newVal === true) { + this.$log.debug("Calling getCategories from refreshRequired") this.getCategories() } }, }, mounted() { + this.$log.info("AppNavi mounted") this.getCategories() }, methods: { @@ -303,6 +306,7 @@ export default { * Fetch and display recipe categories */ async getCategories() { + this.$log.debug("Calling getCategories") const $this = this this.loading.categories = true try { @@ -369,6 +373,7 @@ export default { * Reindex all recipes */ reindex() { + this.$log.debug("Calling reindex") const $this = this if (this.scanningLibrary) { // No repeat clicks until we're done @@ -387,6 +392,7 @@ export default { // This refreshes the current router view in case items in it changed during reindex $this.$router.go() } else { + this.$log.debug("Calling getCategories from reindex") $this.getCategories() } }) diff --git a/src/components/RecipeEdit.vue b/src/components/RecipeEdit.vue index e345343c0..16c9bbc81 100644 --- a/src/components/RecipeEdit.vue +++ b/src/components/RecipeEdit.vue @@ -395,6 +395,7 @@ export default { }, }, mounted() { + this.$log.info("RecipeEdit mounted") const $this = this // Store the initial recipe configuration for possible later use diff --git a/src/components/RecipeView.vue b/src/components/RecipeView.vue index 7ffc449d5..2b24a7ff9 100644 --- a/src/components/RecipeView.vue +++ b/src/components/RecipeView.vue @@ -522,6 +522,7 @@ export default { }, }, mounted() { + this.$log.info("RecipeView mounted") this.setup() // Register data load method hook for access from the controls components this.$root.$off("reloadRecipeView") diff --git a/src/js/logging.js b/src/js/logging.js new file mode 100644 index 000000000..369c5c9e1 --- /dev/null +++ b/src/js/logging.js @@ -0,0 +1,68 @@ +// TODO: Switch to vuejs3-logger when we switch to Vue 3 +import VueLogger from "vuejs-logger" +import moment from "@nextcloud/moment" + +const DEFAULT_LOG_LEVEL = "info" +// How many minutes the logging configuration is valid for +const EXPIRY_MINUTES = 30 +// localStorage keys +const KEY_ENABLED = "COOKBOOK_LOGGING_ENABLED" +const KEY_EXPIRY = "COOKBOOK_LOGGING_EXPIRY" +const KEY_LOG_LEVEL = "COOKBOOK_LOGGING_LEVEL" + +// Check if the logging configuration in local storage has expired +// +// Since the expiry entry is added by us after the first run where +// the enabled entry is detected, this only checks if it has been EXPIRY_MINUTES +// since the first run, not EXPIRY_MINUTES since the user added the entry +// This is a reasonable comprimise to simplify what the user has to do to enable +// logging. We don't want them to have to setup the expiry as well +const isExpired = (timestamp) => { + if (timestamp === null) { + return false + } + + return moment().isAfter(parseInt(timestamp, 10)) +} + +const isEnabled = () => { + const DEFAULT = false + const userValue = localStorage.getItem(KEY_ENABLED) + const expiry = localStorage.getItem(KEY_EXPIRY) + + // Detect the first load after the user has enabled logging + // Set the expiry so the logging isn't enabled forever + if (userValue !== null && expiry === null) { + localStorage.setItem( + KEY_EXPIRY, + moment().add(EXPIRY_MINUTES, "m").valueOf() + ) + } + + if (isExpired(expiry)) { + localStorage.removeItem(KEY_ENABLED) + localStorage.removeItem(KEY_EXPIRY) + + return DEFAULT + } + + // Local storage converts everything to string + // Use JSON.parse to transform "false" -> false + return JSON.parse(userValue) ?? DEFAULT +} + +export default function setupLogging(Vue) { + const logLevel = localStorage.getItem(KEY_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL + + Vue.use(VueLogger, { + isEnabled: isEnabled(), + logLevel, + stringifyArguments: false, + showLogLevel: true, + showMethodName: true, + separator: "|", + showConsoleColors: true, + }) + + Vue.$log.info(`Setting up logging with log level ${logLevel}`) +} diff --git a/src/main.js b/src/main.js index e47631af1..306ea61ee 100644 --- a/src/main.js +++ b/src/main.js @@ -13,6 +13,7 @@ import Vue from "vue" import * as ModalDialogs from "vue-modal-dialogs" import helpers from "cookbook/js/helper" +import setupLogging from "cookbook/js/logging" import router from "./router" import store from "./store" @@ -52,10 +53,13 @@ Vue.use(VueShowdown, { // https://github.com/rlemaigre/vue3-promise-dialog Vue.use(ModalDialogs) +setupLogging(Vue) + // Pass translation engine to Vue Vue.prototype.t = window.t // Start the app once document is done loading +Vue.$log.info("Main is done. Creating App.") const App = Vue.extend(AppMain) new App({ store,