diff --git a/common/core/web/tools/sentry-manager/.gitignore b/common/core/web/tools/sentry-manager/.gitignore new file mode 100644 index 0000000000..a5025863e3 --- /dev/null +++ b/common/core/web/tools/sentry-manager/.gitignore @@ -0,0 +1,17 @@ +# Build products +src/environment.inc.ts + +# Legacy build folders. +output/ +build/ +embedded/ + +# Current build output folder (matches standard node publish location). +dist/ + +# Other local files. +node_modules/ +unit_tests/modernizr.js +**/.idea/**/*.xml +**/*.iml +**/*.kpj.user \ No newline at end of file diff --git a/common/core/web/tools/sentry-manager/package-lock.json b/common/core/web/tools/sentry-manager/package-lock.json new file mode 100644 index 0000000000..daa0f31829 --- /dev/null +++ b/common/core/web/tools/sentry-manager/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "@keymanapp/web-sentry-manager", + "version": "14.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sentry/browser": { + "version": "5.15.5", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.15.5.tgz", + "integrity": "sha512-rqDvjk/EvogfdbZ4TiEpxM/lwpPKmq23z9YKEO4q81+1SwJNua53H60dOk9HpRU8nOJ1g84TMKT2Ov8H7sqDWA==", + "requires": { + "@sentry/core": "5.15.5", + "@sentry/types": "5.15.5", + "@sentry/utils": "5.15.5", + "tslib": "^1.9.3" + } + }, + "@sentry/core": { + "version": "5.15.5", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.15.5.tgz", + "integrity": "sha512-enxBLv5eibBMqcWyr+vApqeix8uqkfn0iGsD3piKvoMXCgKsrfMwlb/qo9Ox0lKr71qIlZVt+9/A2vZohdgnlg==", + "requires": { + "@sentry/hub": "5.15.5", + "@sentry/minimal": "5.15.5", + "@sentry/types": "5.15.5", + "@sentry/utils": "5.15.5", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "5.15.5", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.15.5.tgz", + "integrity": "sha512-zX9o49PcNIVMA4BZHe//GkbQ4Jx+nVofqU/Il32/IbwKhcpPlhGX3c1sOVQo4uag3cqd/JuQsk+DML9TKkN0Lw==", + "requires": { + "@sentry/types": "5.15.5", + "@sentry/utils": "5.15.5", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "5.15.5", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.15.5.tgz", + "integrity": "sha512-zQkkJ1l9AjmU/Us5IrOTzu7bic4sTPKCatptXvLSTfyKW7N6K9MPIIFeSpZf9o1yM2sRYdK7GV08wS2eCT3JYw==", + "requires": { + "@sentry/hub": "5.15.5", + "@sentry/types": "5.15.5", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "5.15.5", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.15.5.tgz", + "integrity": "sha512-F9A5W7ucgQLJUG4LXw1ZIy4iLevrYZzbeZ7GJ09aMlmXH9PqGThm1t5LSZlVpZvUfQ2rYA8NU6BdKJSt7B5LPw==" + }, + "@sentry/utils": { + "version": "5.15.5", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.15.5.tgz", + "integrity": "sha512-Nl9gl/MGnzSkuKeo3QaefoD/OJrFLB8HmwQ7HUbTXb6E7yyEzNKAQMHXGkwNAjbdYyYbd42iABP6Y5F/h39NtA==", + "requires": { + "@sentry/types": "5.15.5", + "tslib": "^1.9.3" + } + }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + } + } +} diff --git a/common/core/web/tools/sentry-manager/package.json b/common/core/web/tools/sentry-manager/package.json new file mode 100644 index 0000000000..68a4379cd5 --- /dev/null +++ b/common/core/web/tools/sentry-manager/package.json @@ -0,0 +1,25 @@ +{ + "name": "@keymanapp/web-sentry-manager", + "version": "14.0.0", + "description": "Common setup for KeymanWeb's Sentry-based error reporting", + "main": "dist/index.js", + "scripts": { + "tsc": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/keymanapp/keyman.git" + }, + "author": "SIL International", + "license": "MIT", + "bugs": { + "url": "https://github.com/keymanapp/keyman/issues" + }, + "homepage": "https://github.com/keymanapp/keyman#readme", + "devDependencies": { + "typescript": "^3.8.3" + }, + "dependencies": { + "@sentry/browser": "^5.15.5" + } +} diff --git a/common/core/web/tools/sentry-manager/src/build.sh b/common/core/web/tools/sentry-manager/src/build.sh new file mode 100755 index 0000000000..b1adcfa1ab --- /dev/null +++ b/common/core/web/tools/sentry-manager/src/build.sh @@ -0,0 +1,66 @@ +#! /bin/bash +# +# Compiles development-related KeymanWeb resources for use with developing/running tests. +# - the Recorder module (for engine tests) +# - the DOM module (for touch-alias and element-interface tests) + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")" +. "$(dirname "$THIS_SCRIPT")/../../../../../../resources/build/build-utils.sh" +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +SCRIPT_DIR="$(dirname "$THIS_SCRIPT")" + +display_usage ( ) { + echo "build.sh [-skip-dependency-install]" + echo + echo " -skip-dependency-install skips the `lerna bootstrap` dependency check." + echo " (or -S) Intended for use when this script is called by another build script." + echo "" + echo " If more than one target is specified, the last one will take precedence." + exit 1 +} + +# Establish default build parameters +set_default_vars ( ) { + FETCH_DEPS=true +} + +set_default_vars + +# Parse args +while [[ $# -gt 0 ]] ; do + key="$1" + case $key in + -skip-package-install|-S) + set_default_vars + FETCH_DEPS=false + ;; + esac + shift # past argument +done + +if [ $FETCH_DEPS = true ]; then + verify_npm_setup +fi + +# Definition of global compile constants +OUTPUT_DIR="dist" +OUTPUT="index.js" + +# Ensures that we rely first upon the local npm-based install of Typescript. +# (Facilitates automated setup for build agents.) +PATH="../node_modules/.bin:$PATH" + +# Ensure the environment definition variables are available. +exportEnvironmentDefinitionTS + +compiler="npm run tsc --" +compilecmd="$compiler" + +$compilecmd -p "$SCRIPT_DIR/tsconfig.json" +if [ $? -ne 0 ]; then + fail "Compilation of package for Sentry integration with KeymanWeb failed." +fi \ No newline at end of file diff --git a/common/core/web/tools/sentry-manager/src/index.ts b/common/core/web/tools/sentry-manager/src/index.ts new file mode 100644 index 0000000000..988757031a --- /dev/null +++ b/common/core/web/tools/sentry-manager/src/index.ts @@ -0,0 +1,147 @@ +/// + +namespace com.keyman { + /** + * Controls whether or not the generated Sentry event is logged to the console (true) + * or sent to our Sentry server (false). + */ + let DEBUG = false; + + type Options = typeof KeymanSentryManager.DEFAULT_OPTIONS; + + export class KeymanSentryManager { + keymanPlatform: string; + + static STANDARD_ALIASABLE_FILES = { + 'keymanweb.js': 'keymanweb.js', + 'kmwuibutton.js': 'kmwuibutton.js', + 'kmwuifloat.js': 'kmwuifloat.js', + 'kmwuitoggle.js': 'kmwuitoggle.js', + 'kmwuitoolbar.js': 'kmwuitoolbar.js' + // Also add entries for the naming system used by Android and iOS - and map them to the EMBEDDED upload, not the std 'native' one. + } + + static DEFAULT_OPTIONS = { + hostPlatform: "native-web" + } + + constructor(options: Options = KeymanSentryManager.DEFAULT_OPTIONS) { + this.keymanPlatform = options.hostPlatform; + } + + // If we've recognized one of our source files, + aliasFilename(filename: string): string|null { + if(!this.mayAlias(filename)) { + return null; + } + + filename = KeymanSentryManager.STANDARD_ALIASABLE_FILES[filename]; + + switch(location.protocol) { + case 'http:': + return 'http://' + location.host + '/' + filename; + case 'file:': + return 'file:///' + filename; + default: + return null; + } + } + + mayAlias(filename: string): boolean { + return !!KeymanSentryManager.STANDARD_ALIASABLE_FILES[filename]; + } + + // Filters all expected but unnecessary path prefixes, affixes, and suffixes reported by Sentry from our products. + // This allows us to mask all different sorts of installations with a single uploaded path. + // Modifies original object. + pathFilter(event: any) { + // Get the underlying JS error. + let exception = event.exception; + + // Iterate through all wrapped exceptions. + for(let e of exception.values) { + for(let frame of e.stacktrace.frames) { + let URL = frame.filename as string; + let filename: string = ''; + try { + filename = URL.substr(URL.lastIndexOf('/')+1); + if(this.mayAlias(filename)) { + // Make the file look like it's at the host's root so that the Sentry server will recognize it + // regardless of actual filepath position. + frame.filename = this.aliasFilename(filename) || frame.filename; + } + } catch { + // not sure what to do yet, but... don't make more errors in the error handler. That's a bad idea. + } + } + } + } + + // Attaches some useful debugging information to the specified object, pass-by-reference style. + attachEventMetadata(event: any) { + // Ensure that the 'extra' object exists. (May not exist for synthetic/custom Errors.) + event.extra = event.extra || {}; + event.extra.keymanState = window['keyman']['getDebugInfo'](); + event.extra.keymanHostPlatform = this.keymanPlatform; + } + + /** + * Pre-processes a Sentry event object (in-place) to provide more metadata and enhance + * the Sentry server's ability to match the error against release artifacts. + * @param event A Sentry-generated event + */ + prepareEvent(event: any): boolean { + this.pathFilter(event); + this.attachEventMetadata(event); + + if(DEBUG) { + console.log("DEBUG: event object for Sentry") + console.log(event); + return false; //event + } else { + return true; + } + } + + /** + * Allows debugging our custom event preparation code without bombarding Sentry with errors + * during development. + * + * Note that Sentry expects us either to return the event object to be sent or to return `null` + * if we want to prevent the event from being sent to the server. + * @param event + */ + prepareEventDebugWrapper(event: any) { + if(DEBUG) { + try { + if(this.prepareEvent(event)) { + return event; + } else { + return null; + } + } catch(err) { + console.log(err); + } + } else { + // If not in DEBUG mode, simply forward to the actual preparer; we should be notified + // of any event-prep errors that may occur. + if(this.prepareEvent(event)) { + return event; + } else { + return null; + } + } + } + + init() { + // Do the actual Sentry initialization. + //@ts-ignore + Sentry.init({ + beforeSend: this.prepareEventDebugWrapper.bind(this), + debug: DEBUG, + dsn: 'https://cf96f32d107c4286ab2fd82af49c4d3b@sentry.keyman.com/11', // keyman-web DSN + release: com.keyman.environment.SENTRY_RELEASE + }); + } + } +} \ No newline at end of file diff --git a/common/core/web/tools/sentry-manager/src/tsconfig.json b/common/core/web/tools/sentry-manager/src/tsconfig.json new file mode 100644 index 0000000000..b23d357baa --- /dev/null +++ b/common/core/web/tools/sentry-manager/src/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowJs": true, + "module": "none", + "outDir": "../dist/", + "inlineSources": true, + "sourceMap": true, + "target": "es5", + "lib": ["es6", "dom"], + "outFile": "../dist/index.js" + }, + "files": [ + "environment.inc.ts", + "index.ts" + ] +} \ No newline at end of file diff --git a/lerna.json b/lerna.json index 080660cc0c..0ac9963322 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,7 @@ "common/core/web/input-processor", "common/core/web/keyboard-processor", "common/core/web/tools/recorder", + "common/core/web/tools/sentry-manager", "common/core/web/utils", "common/lexical-model-types", "common/predictive-text", diff --git a/resources/build/build-utils.sh b/resources/build/build-utils.sh index bff6e82974..351c29d10f 100755 --- a/resources/build/build-utils.sh +++ b/resources/build/build-utils.sh @@ -195,6 +195,7 @@ exportEnvironmentDefinitionTS ( ) { namespace com.keyman.environment { export var VERSION = \"$VERSION_RELEASE\"; export var BUILD = $VERSION_PATCH; + export var SENTRY_RELEASE = \"release-$VERSION_WITH_TAG\"; } " >> $ENVIRONMENT_TS } \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index aa5301dfd8..94b30729b7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -4,6 +4,70 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sentry/cli": { + "version": "1.52.3", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.52.3.tgz", + "integrity": "sha512-QOSIg5hxAEa6v6H7oEeF6A/Rpa0wloMhbu0Qed6zHv3lyoqf0Z34Kq2jCXdqGsOE3IzkO+3CNy81F6361j5TKg==", + "dev": true, + "requires": { + "https-proxy-agent": "^5.0.0", + "mkdirp": "^0.5.5", + "node-fetch": "^2.6.0", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "@types/node": { "version": "11.9.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.4.tgz", @@ -1552,12 +1616,6 @@ } } }, - "mocha-teamcity-reporter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mocha-teamcity-reporter/-/mocha-teamcity-reporter-3.0.0.tgz", - "integrity": "sha512-FyGgmtFfW2nDwEZU3mrjQShAAK/zhGivwY4HCsqoDoyeS8vV8HGdq1Dn2P+SFaIoCeXTQ0Z+5xVRyikYaKrW5w==", - "dev": true - }, "modernizr": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/modernizr/-/modernizr-3.7.1.tgz", @@ -1585,6 +1643,12 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", "dev": true }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1736,6 +1800,18 @@ "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", "dev": true }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "ps-tree": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.1.tgz", diff --git a/web/package.json b/web/package.json index bf89b75d2f..810f7fcde9 100644 --- a/web/package.json +++ b/web/package.json @@ -18,6 +18,8 @@ }, "homepage": "https://github.com/keymanapp/keyman#readme", "devDependencies": { + "@keymanapp/web-sentry-manager": "^14.0.0", + "@sentry/cli": "^1.52.3", "chai": "^4.2.0", "google-closure-compiler-java": "^20200224.0.0", "html2canvas": "^1.0.0-alpha.12", @@ -45,7 +47,8 @@ "karma": "karma", "lerna": "cd ../ && npm run lerna --", "mocha": "mocha", - "modernizr": "modernizr" + "modernizr": "modernizr", + "sentry-cli": "sentry-cli" }, "dependencies": { "@keymanapp/input-processor": "^14.0.78", diff --git a/web/source/build.sh b/web/source/build.sh index 1c41984a41..eb3e485f7e 100755 --- a/web/source/build.sh +++ b/web/source/build.sh @@ -206,6 +206,13 @@ INTERMEDIATE="../intermediate" SOURCE="." NODE_SOURCE="source" +SENTRY_RELEASE_VERSION="release-$VERSION_WITH_TAG" + +export SENTRY_URL="https://sentry.keyman.com" +export SENTRY_ORG=keyman +export SENTRY_PROJECT=keyman-web +export SENTRY_LOG_LEVEL=info + readonly WEB_OUTPUT readonly EMBED_OUTPUT readonly SOURCE @@ -227,6 +234,7 @@ set_default_vars ( ) { BUILD_DEBUG_EMBED=false BUILD_COREWEB=true DO_MINIFY=true + DO_NATIVE_SENTRY=true FETCH_DEPS=true } @@ -264,6 +272,7 @@ while [[ $# -gt 0 ]] ; do BUILD_FULLWEB=false BUILD_UI=false BUILD_COREWEB=false + DO_NATIVE_SENTRY=false ;; -web) set_default_vars @@ -276,12 +285,14 @@ while [[ $# -gt 0 ]] ; do BUILD_COREWEB=false BUILD_FULLWEB=false BUILD_DEBUG_EMBED=true + DO_NATIVE_SENTRY=false ;; -h|-?) display_usage ;; -no_minify) DO_MINIFY=false + DO_NATIVE_SENTRY=false ;; -clean) clean @@ -314,6 +325,7 @@ if [ $DO_MINIFY = true ]; then exit 1 fi + # Only run if BOTH cases are true b/c it's a minification-focused 'dependency'. if [ $FETCH_DEPS = true ]; then # Also, build our sourcemap-root tool for cleaning up the minified version's sourcemaps. echo "Compiling build tools for minified build products" @@ -324,6 +336,14 @@ fi if [ $BUILD_CORE = true ]; then CORE_FLAGS="-skip-package-install" + + # Build the sentry-manager module - it's used in embedded contexts and on one testing page. + echo "${TERM_HEADING}Compiling KeymanWeb's sentry-manager module...${NORMAL}" + pushd ../../common/core/web/tools/sentry-manager/src + ./build.sh $CORE_FLAGS || fail "Failed to compile the sentry-manager module" + popd + echo "${TERM_HEADING}sentry-manager module compiled successfully.${NORMAL}" + if [ $BUILD_LMLAYER = false ]; then CORE_FLAGS="$CORE_FLAGS -test" fi @@ -381,6 +401,13 @@ if [ $BUILD_EMBED = true ]; then echo echo KMEA/KMEI version $VERSION compiled and saved under $EMBED_OUTPUT echo + + # We always publish "embedded" mode to Sentry when minifying. + pushd $EMBED_OUTPUT + echo "Uploading to Sentry..." + npm run sentry-cli -- releases files "$SENTRY_RELEASE_VERSION" upload-sourcemaps --strip-common-prefix release/embedded --rewrite --ext js --ext map --ext ts || fail "Sentry upload failed." + echo "Upload successful." + popd fi fi @@ -462,6 +489,16 @@ if [ $BUILD_UI = true ]; then fi fi +# Has its own flag due to complex logic. Should run if $DO_MINIFY && ($BUILD_COREWEB || $BUILD_UI) +# If only one of the two (COREWEB, UI) is run, we assume the un-run portion is 'current' and still upload it. +if [ $DO_NATIVE_SENTRY = true ]; then + pushd $WEB_OUTPUT + echo "Uploading to Sentry..." + npm run sentry-cli -- releases files "$SENTRY_RELEASE_VERSION" upload-sourcemaps --strip-common-prefix release/web/ --rewrite --ext js --ext map --ext ts || fail "Sentry upload failed." + echo "Upload successful." + popd +fi + if [ $BUILD_DEBUG_EMBED = true ]; then # We currently have an issue with sourcemaps for minified versions. # We should use the unminified one instead for now. @@ -470,4 +507,3 @@ if [ $BUILD_DEBUG_EMBED = true ]; then cp $EMBED_OUTPUT_NO_MINI/keyman.js.map $EMBED_OUTPUT/keyman.js.map echo Uncompiled embedded application saved as keyman.js fi - diff --git a/web/source/keyboards/kmwkeyboards.ts b/web/source/keyboards/kmwkeyboards.ts index a1e41fb63a..e6ef3cfa18 100644 --- a/web/source/keyboards/kmwkeyboards.ts +++ b/web/source/keyboards/kmwkeyboards.ts @@ -372,7 +372,7 @@ namespace com.keyman.keyboards { } this.doBeforeKeyboardChange(PInternalName,PLgCode); - let p = this._SetActiveKeyboard(PInternalName,PLgCode,true); + let p: Promise = this._SetActiveKeyboard(PInternalName,PLgCode,true); if(this.keymanweb.domManager.getLastActiveElement() != null) { this.keymanweb.domManager.focusLastActiveElement(); // TODO: Resolve without need for the cast. } @@ -383,6 +383,14 @@ namespace com.keyman.keyboards { // } this.doKeyboardChange(PInternalName, PLgCode); + p.catch(function() { + // Rejection indicates a failure of the keyboard to load. + // + // In case p's rejection is never caught, throwing this error will generate logs that shows up + // in Sentry or in the console, with useful information for debugging either way. + throw new Error("Unable to load keyboard with internal name \"" + PInternalName + "\", language code \"" + PLgCode + "\"."); + }) + return p; } diff --git a/web/source/kmwbase.ts b/web/source/kmwbase.ts index 0ca1f26083..418078c5d2 100644 --- a/web/source/kmwbase.ts +++ b/web/source/kmwbase.ts @@ -177,6 +177,25 @@ namespace com.keyman { dom.DOMEventHandlers.states = new dom.CommonDOMStates(); } + /** + * Returns a generalized metadata object about the state of KMW for use with error reporting. + */ + ['getDebugInfo']() { + let metadata = { + attachType: this.options.attachType, + device: this.util.device, + initialized: this.initialized, + isEmbedded: this.isEmbedded, + ui: this.ui ? this.ui.name : null + } + + if(this.util.device.touchable) { + metadata.ui = 'touch'; + } + + return metadata; + } + /** * Expose font testing to allow checking that SpecialOSK or custom font has * been correctly loaded by browser diff --git a/web/testing/index.html b/web/testing/index.html index 5084af2234..5a3a76b5ec 100644 --- a/web/testing/index.html +++ b/web/testing/index.html @@ -19,7 +19,7 @@ -

KeymanWeb 12 Testing

+

KeymanWeb 14 Testing

Test unminified Keymanweb

Test unminified Keymanweb in manual-attachment mode.

Tests keyboard error-handling functionality

@@ -30,6 +30,7 @@

Prediction UI testing

Toolbar UI testing

Tests keyboard documentation rendering.

+

Tests Sentry error-reporting functionality

Auto-attachment mode tests

Test desktop MutationObserver functionality

Light test for touch-based MutationObserver functionality

diff --git a/web/testing/sentry-integration/errorhdr.js b/web/testing/sentry-integration/errorhdr.js new file mode 100644 index 0000000000..b784f6a476 --- /dev/null +++ b/web/testing/sentry-integration/errorhdr.js @@ -0,0 +1,42 @@ +// JavaScript Document samplehdr.js: Keyboard management for KeymanWeb demonstration pages + +/* + This script is designed to test KeymanWeb error message handling. +*/ + + function loadKeyboards() + { + var kmw=keyman; + + // We start by adding a keyboard correctly. It's best to include a 'control' in our experiment. + kmw.addKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'}, + filename:'../us-1.0.js'}); + + // Insert a keyboard that cannot be found. + kmw.addKeyboards({id:'lao_2008_basic',name:'wrong-filename', + languages:{ + id:'lo',name:'debugging',region:'Asia', + font:{family:'LaoWeb',source:['../font/saysettha_web.ttf','../font/saysettha_web.woff','../font/saysettha_web.eot']} + }, + filename:'./missing_file.js' // Intentional error - the file doesn't exist, so the + + + + + + + + + + + + + + + + +

KeymanWeb Sample Page - Sentry Setup test host

+ +
+ +
+ +

Type in your language in this text area:

+ + +

or in this input field:

+ + + +

Add a keyboard by keyboard name:

+ + + +

Add a keyboard by BCP-47 language code:

+ + + +

Add a keyboard by language name:

+ + + +

Return to testing home page

+
+ + + + + diff --git a/web/testing/sentry-integration/timeout.js b/web/testing/sentry-integration/timeout.js new file mode 100644 index 0000000000..2af6bf907a --- /dev/null +++ b/web/testing/sentry-integration/timeout.js @@ -0,0 +1,10 @@ +(function() { + var me = document.currentScript; + console.log(me); + document.body.addEventListener('load', function(e) { + // Prevent the element's onload event from firing + if(e.srcElement == me || e.target == me) { + e.cancelBubble = true; + } + }, {capture: true}); +})(); \ No newline at end of file diff --git a/web/testing/sentry-integration/unparsable.js b/web/testing/sentry-integration/unparsable.js new file mode 100644 index 0000000000..76dd4f3a1d --- /dev/null +++ b/web/testing/sentry-integration/unparsable.js @@ -0,0 +1 @@ +// As there is no parsable keyboard here, we need to report that we couldn't register the requested keyboard. \ No newline at end of file diff --git a/web/tools/recorder/build.sh b/web/tools/recorder/build.sh index b62445dfc7..7d90e80384 100755 --- a/web/tools/recorder/build.sh +++ b/web/tools/recorder/build.sh @@ -7,11 +7,12 @@ ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")" -SCRIPT_DIR="$(dirname "$THIS_SCRIPT")" . "$(dirname "$THIS_SCRIPT")/../../../resources/build/build-utils.sh" . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" ## END STANDARD BUILD SCRIPT INCLUDE +SCRIPT_DIR="$(dirname "$THIS_SCRIPT")" + # Ensure the dependencies are downloaded. --no-optional should help block fsevents warnings. verify_npm_setup