├── test └── tslint.json ├── .npmignore ├── assets ├── project.png └── sentry-screetshot.png ├── uapp-demo ├── ide.config.js ├── static │ └── logo.png ├── vite.config.js ├── uni.promisify.adaptor.js ├── uno.config.js ├── package.json ├── main.js ├── pages │ └── index │ │ └── index.vue ├── common │ ├── config.js │ ├── utils │ │ └── login.js │ └── http │ │ └── index.js ├── pages.json ├── index.html ├── App.vue ├── uni.scss ├── README.md └── manifest.json ├── src ├── transports │ ├── index.ts │ ├── base.ts │ └── xhr.ts ├── version.ts ├── integrations │ ├── tslint.json │ ├── index.ts │ ├── ignoreMpcrawlerErrors.ts │ ├── router.ts │ ├── linkederrors.ts │ ├── system.ts │ ├── trycatch.ts │ └── globalhandlers.ts ├── flags.ts ├── index.ts ├── client.ts ├── crossPlatform.ts ├── backend.ts ├── parsers.ts ├── eventbuilder.ts ├── sdk.ts ├── helpers.ts └── tracekit.ts ├── tslint.json ├── webpack.config.min.js ├── tsconfig.json ├── tsconfig.esm.json ├── .gitignore ├── scripts └── versionbump.js ├── webpack.common.js ├── LICENSE ├── package.json └── README.md /test/tslint.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !/dist/**/* 3 | !/esm/**/* 4 | *.tsbuildinfo -------------------------------------------------------------------------------- /assets/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uappx/sentry-uniapp/HEAD/assets/project.png -------------------------------------------------------------------------------- /uapp-demo/ide.config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | "paths": { 3 | "@/*": "./*" 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /uapp-demo/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uappx/sentry-uniapp/HEAD/uapp-demo/static/logo.png -------------------------------------------------------------------------------- /assets/sentry-screetshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uappx/sentry-uniapp/HEAD/assets/sentry-screetshot.png -------------------------------------------------------------------------------- /src/transports/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseTransport } from "./base"; 2 | export { XHRTransport } from "./xhr"; 3 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sentry/typescript/tslint", 3 | "rules": { 4 | "object-literal-sort-keys": false 5 | } 6 | } -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json'; 2 | 3 | export const SDK_NAME = "sentry.javascript.uniapp"; 4 | export const SDK_VERSION = version 5 | -------------------------------------------------------------------------------- /src/integrations/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../tslint.json"], 3 | "rules": { 4 | "no-unsafe-any": false, 5 | "only-arrow-functions": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /uapp-demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import uni from '@dcloudio/vite-plugin-uni' 3 | import UnoCSS from 'unocss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | uni(), 8 | UnoCSS() 9 | ], 10 | ssr: { 11 | format: 'cjs' 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /src/integrations/index.ts: -------------------------------------------------------------------------------- 1 | export { GlobalHandlers } from "./globalhandlers"; 2 | export { TryCatch } from "./trycatch"; 3 | export { LinkedErrors } from "./linkederrors"; 4 | 5 | export { System } from "./system"; 6 | export { Router } from "./router"; 7 | export { IgnoreMpcrawlerErrors } from "./ignoreMpcrawlerErrors"; 8 | -------------------------------------------------------------------------------- /webpack.config.min.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require("webpack-merge"); 3 | const common = require("./webpack.common.js"); 4 | 5 | module.exports = merge(common, { 6 | output: { 7 | filename: "sentry-uniapp.min.js", 8 | path: path.resolve(__dirname, "./dist"), 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@sentry/typescript/tsconfig.json", 3 | "include": ["src/**/*", "test/**/*"], 4 | "exclude": ["./examples"], 5 | "compilerOptions": { 6 | "outDir": "dist", 7 | "module": "commonjs", 8 | "target": "es5", 9 | "resolveJsonModule": true, 10 | "sourceMap": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@sentry/typescript/tsconfig.json", 3 | "include": [ 4 | "src/**/*", 5 | "test/**/*" 6 | ], 7 | "exclude": [ 8 | "./examples" 9 | ], 10 | "compilerOptions": { 11 | "outDir": "esm", 12 | "module": "es6", 13 | "target": "es5", 14 | "resolveJsonModule": true, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /uapp-demo/uni.promisify.adaptor.js: -------------------------------------------------------------------------------- 1 | uni.addInterceptor({ 2 | returnValue (res) { 3 | if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { 4 | return res; 5 | } 6 | return new Promise((resolve, reject) => { 7 | res.then((res) => res[0] ? reject(res[0]) : resolve(res[1])); 8 | }); 9 | }, 10 | }); -------------------------------------------------------------------------------- /uapp-demo/uno.config.js: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | transformerDirectives, 4 | transformerVariantGroup, 5 | } from 'unocss' 6 | 7 | import { presetUni } from '@uni-helper/unocss-preset-uni' 8 | 9 | export default defineConfig({ 10 | presets: [ 11 | presetUni({ 12 | attributify: false 13 | }), 14 | ], 15 | transformers: [ 16 | transformerDirectives(), 17 | transformerVariantGroup(), 18 | ], 19 | }) 20 | -------------------------------------------------------------------------------- /uapp-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uappx", 3 | "version": "1.0.0", 4 | "description": "uapp 快速开发工程模版,模版里集成了 unocss/tailwindcss, uvui等", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Qisen Yz ", 10 | "license": "ISC", 11 | "dependencies": { 12 | "sentry-uniapp": "^1.0.11", 13 | "@climblee/uv-ui": "^1.1.20", 14 | "@uni-helper/unocss-preset-uni": "^0.2.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | packages/*/package-lock.json 4 | 5 | # build and test 6 | dist/ 7 | esm/ 8 | build/ 9 | packages/*/dist/ 10 | packages/*/esm/ 11 | coverage/ 12 | scratch/ 13 | *.pyc 14 | *.tsbuildinfo 15 | 16 | # logs 17 | yarn-error.log 18 | npm-debug.log 19 | lerna-debug.log 20 | local.log 21 | 22 | # ide 23 | .idea 24 | *.sublime-* 25 | 26 | # misc 27 | .DS_Store 28 | ._* 29 | .Spotlight-V100 30 | .Trashes 31 | 32 | .rpt2_cache 33 | 34 | lint-results.json 35 | 36 | # legacy 37 | tmp.js 38 | -------------------------------------------------------------------------------- /scripts/versionbump.js: -------------------------------------------------------------------------------- 1 | const replace = require('replace-in-file'); 2 | const packageJson = require(`${process.cwd()}/package.json`); 3 | 4 | const files = process.argv.slice(2); 5 | if (files.length === 0) { 6 | console.error('Please provide files to bump'); 7 | process.exit(1); 8 | } 9 | 10 | replace({ 11 | files: files, 12 | from: /\d+\.\d+.\d+(?:-\w+(?:\.\w+)?)?/g, 13 | to: packageJson.version, 14 | }) 15 | .then(([{ file, hasChanged }]) => { 16 | console.log('Modified files:', file, hasChanged); 17 | }) 18 | .catch(error => { 19 | console.error('Error occurred:', error); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /uapp-demo/main.js: -------------------------------------------------------------------------------- 1 | import App from './App' 2 | 3 | import uvUI from '@climblee/uv-ui' 4 | import { Request } from '@/common/http/index' 5 | import 'uno.css' 6 | 7 | // #ifndef VUE3 8 | import Vue from 'vue' 9 | import './uni.promisify.adaptor' 10 | Vue.config.productionTip = false 11 | 12 | Vue.use(uvUI); 13 | 14 | App.mpType = 'app' 15 | const app = new Vue({ 16 | ...App 17 | }) 18 | app.$mount() 19 | // #endif 20 | 21 | // #ifdef VUE3 22 | import { createSSRApp } from 'vue' 23 | export function createApp() { 24 | const app = createSSRApp(App) 25 | app.use(uvUI) 26 | Request(app) 27 | 28 | return { 29 | app 30 | } 31 | } 32 | // #endif -------------------------------------------------------------------------------- /uapp-demo/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /uapp-demo/common/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: Qisen Yz 3 | * Github: https://github.com/uappkit/uapp 4 | * 5 | * Copyright(c) 2022 - 2024, uapp.dev 6 | */ 7 | 8 | // API_BASE_URL: API请求的URL域名前缀 9 | export const API_BASE_URL = process.env.NODE_ENV === 'development' 10 | ? 'https://api-dev.code0xff.com/' 11 | : 'https://api.code0xff.com/' 12 | 13 | // LOGIN_PAGE: 改为自己的登录页面,HTTP请求401时,会跳转到登录页 14 | export const LOGIN_PAGE = '/pages/login/login' 15 | 16 | // HOME_PAGE: 成功登录后的首页,或从登录导航栏跳转到首页 17 | export const HOME_PAGE = '/pages/index/index' 18 | 19 | // 使用 uni.setStorageSync(TOKEN_KEY) 保存用户的 token 20 | export const TOKEN_KEY = 'token' 21 | -------------------------------------------------------------------------------- /uapp-demo/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "easycom": { 3 | "autoscan": true, 4 | "custom": { 5 | "^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue" 6 | } 7 | }, 8 | "pages": [ 9 | //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages 10 | { 11 | "path": "pages/index/index", 12 | "style": { 13 | "navigationBarTitleText": "uapp" 14 | } 15 | } 16 | ], 17 | "globalStyle": { 18 | "navigationBarTextStyle": "black", 19 | "navigationBarTitleText": "uapp", 20 | "navigationBarBackgroundColor": "#F8F8F8", 21 | "backgroundColor": "#F8F8F8" 22 | }, 23 | "uniIdRouter": {} 24 | } 25 | -------------------------------------------------------------------------------- /uapp-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /uapp-demo/common/utils/login.js: -------------------------------------------------------------------------------- 1 | import { HOME_PAGE, LOGIN_PAGE } from '@/common/config' 2 | 3 | export const goLoginPage = () => { 4 | uni.navigateTo({ 5 | url: LOGIN_PAGE, 6 | }) 7 | } 8 | 9 | export const goHomePage = () => { 10 | uni.switchTab({ 11 | url: HOME_PAGE, 12 | }) 13 | } 14 | 15 | export const logoutAppleAccount = async () => { 16 | new Promise((resolve, reject) => { 17 | plus.oauth.getServices((services) => { 18 | let auths = [] 19 | for (let service of services) { 20 | auths[service.id] = service 21 | } 22 | 23 | if (!auths['apple']) { 24 | return reject('no apple provider') 25 | } 26 | 27 | auths['apple'].logout(res => { 28 | resove(res) 29 | }, err => { 30 | reject(err) 31 | }) 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/flags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file defines flags and constants that can be modified during compile time in order to facilitate tree shaking 3 | * for users. 4 | * 5 | * Debug flags need to be declared in each package individually and must not be imported across package boundaries, 6 | * because some build tools have trouble tree-shaking imported guards. 7 | * 8 | * As a convention, we define debug flags in a `flags.ts` file in the root of a package's `src` folder. 9 | * 10 | * Debug flag files will contain "magic strings" like `__SENTRY_DEBUG__` that may get replaced with actual values during 11 | * our, or the user's build process. Take care when introducing new flags - they must not throw if they are not 12 | * replaced. 13 | */ 14 | 15 | declare const __SENTRY_DEBUG__: boolean; 16 | 17 | /** Flag that is true for debug builds, false otherwise. */ 18 | export const IS_DEBUG_BUILD = typeof __SENTRY_DEBUG__ === 'undefined' ? true : __SENTRY_DEBUG__; 19 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "production", 3 | entry: "./src/index.ts", 4 | output: { 5 | filename: "sentry-uniapp.min.js", 6 | library: "Sentry", 7 | libraryTarget: "commonjs2" 8 | }, 9 | externals: { 10 | '@system.app': 'crossPlatform', 11 | '@system.device': 'crossPlatform', 12 | '@system.battery': 'crossPlatform', 13 | '@system.router': 'crossPlatform', 14 | '@system.fetch': 'crossPlatform' 15 | }, 16 | resolve: { 17 | extensions: [".tsx", ".ts", ".js"] 18 | }, 19 | watchOptions: { 20 | ignored: /node_modules|examples/, //忽略不用监听变更的目录 21 | aggregateTimeout: 300, // 文件发生改变后多长时间后再重新编译(Add a delay before rebuilding once the first file changed ) 22 | poll: 1000 //每秒询问的文件变更的次数 23 | }, 24 | plugins: [], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | loader: "ts-loader", 30 | exclude: /node_modules/ 31 | } 32 | ] 33 | }, 34 | devtool: "source-map" 35 | }; 36 | -------------------------------------------------------------------------------- /src/transports/base.ts: -------------------------------------------------------------------------------- 1 | import { API } from "@sentry/core"; 2 | import { Event, Response, Transport, TransportOptions } from "@sentry/types"; 3 | import { makePromiseBuffer, PromiseBuffer, SentryError } from "@sentry/utils"; 4 | 5 | /** Base Transport class implementation */ 6 | export abstract class BaseTransport implements Transport { 7 | /** 8 | * @inheritDoc 9 | */ 10 | public url: string; 11 | 12 | /** A simple buffer holding all requests. */ 13 | protected readonly _buffer: PromiseBuffer = makePromiseBuffer(30); 14 | 15 | public constructor(public options: TransportOptions) { 16 | this.url = new API(this.options.dsn).getStoreEndpointWithUrlEncodedAuth(); 17 | } 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | public sendEvent(_: Event): PromiseLike { 23 | throw new SentryError( 24 | "Transport Class has to implement `sendEvent` method" 25 | ); 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public close(timeout?: number): PromiseLike { 32 | return this._buffer.drain(timeout); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Breadcrumb, 3 | BreadcrumbHint, 4 | Request, 5 | SdkInfo, 6 | Event, 7 | EventHint, 8 | EventStatus, 9 | Exception, 10 | Response, 11 | Severity, 12 | StackFrame, 13 | Stacktrace, 14 | Thread, 15 | User, 16 | } from "@sentry/types"; 17 | 18 | export { 19 | addGlobalEventProcessor, 20 | addBreadcrumb, 21 | captureException, 22 | captureEvent, 23 | captureMessage, 24 | configureScope, 25 | getHubFromCarrier, 26 | getCurrentHub, 27 | Hub, 28 | Scope, 29 | setContext, 30 | setExtra, 31 | setExtras, 32 | setTag, 33 | setTags, 34 | setUser, 35 | withScope 36 | } from "@sentry/core"; 37 | 38 | export { SDK_NAME, SDK_VERSION } from "./version"; 39 | export { 40 | defaultIntegrations, 41 | init, 42 | lastEventId, 43 | showReportDialog, 44 | flush, 45 | close, 46 | wrap 47 | } from "./sdk"; 48 | export { MiniappOptions } from "./backend"; 49 | export { MiniappClient, ReportDialogOptions } from "./client"; 50 | 51 | import * as Integrations from "./integrations/index"; 52 | import * as Transports from "./transports/index"; 53 | 54 | export { Integrations, Transports }; 55 | -------------------------------------------------------------------------------- /src/integrations/ignoreMpcrawlerErrors.ts: -------------------------------------------------------------------------------- 1 | import { addGlobalEventProcessor, getCurrentHub } from "@sentry/core"; 2 | import { Event, Integration } from "@sentry/types"; 3 | 4 | import { appName, sdk } from "../crossPlatform"; 5 | 6 | /** 7 | * IgnoreMpcrawlerErrors 8 | * 9 | * https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/sitemap.html 10 | */ 11 | export class IgnoreMpcrawlerErrors implements Integration { 12 | /** 13 | * @inheritDoc 14 | */ 15 | public name: string = IgnoreMpcrawlerErrors.id; 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | public static id: string = "IgnoreMpcrawlerErrors"; 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public setupOnce(): void { 26 | addGlobalEventProcessor((event: Event) => { 27 | if ( 28 | getCurrentHub().getIntegration(IgnoreMpcrawlerErrors) && 29 | appName === "wechat" && 30 | sdk.getLaunchOptionsSync 31 | ) { 32 | const options = sdk.getLaunchOptionsSync(); 33 | 34 | if (options.scene === 1129) { 35 | return null; 36 | } 37 | } 38 | 39 | return event; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/transports/xhr.ts: -------------------------------------------------------------------------------- 1 | import { Event, Response } from "@sentry/types"; 2 | import { eventStatusFromHttpCode } from '@sentry/utils'; 3 | 4 | import { sdk } from "../crossPlatform"; 5 | 6 | import { BaseTransport } from "./base"; 7 | 8 | /** `XHR` based transport */ 9 | export class XHRTransport extends BaseTransport { 10 | /** 11 | * @inheritDoc 12 | */ 13 | public sendEvent(event: Event): PromiseLike { 14 | const request = sdk.request || sdk.httpRequest; 15 | 16 | return this._buffer.add( 17 | () => new Promise((resolve, reject) => { 18 | // tslint:disable-next-line: no-unsafe-any 19 | request({ 20 | url: this.url, 21 | method: "POST", 22 | data: JSON.stringify(event), 23 | header: { 24 | "content-type": "application/json" 25 | }, 26 | success(res: { statusCode: number }): void { 27 | resolve({ 28 | status: eventStatusFromHttpCode(res.statusCode) 29 | }); 30 | }, 31 | fail(error: object): void { 32 | reject(error); 33 | } 34 | }); 35 | }) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /uapp-demo/App.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Sentry 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/integrations/router.ts: -------------------------------------------------------------------------------- 1 | import { addGlobalEventProcessor, getCurrentHub } from "@sentry/core"; 2 | import { Event, Integration } from "@sentry/types"; 3 | 4 | declare const getCurrentPages: any; 5 | 6 | /** JSDoc */ 7 | interface RouterIntegrations { 8 | enable?: boolean; 9 | } 10 | 11 | /** UserAgent */ 12 | export class Router implements Integration { 13 | /** 14 | * @inheritDoc 15 | */ 16 | public name: string = Router.id; 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | public static id: string = "Router"; 22 | 23 | /** JSDoc */ 24 | private readonly _options: RouterIntegrations; 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public constructor(options?: RouterIntegrations) { 30 | this._options = { 31 | enable: true, 32 | ...options, 33 | }; 34 | } 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | public setupOnce(): void { 40 | addGlobalEventProcessor((event: Event) => { 41 | if (getCurrentHub().getIntegration(Router)) { 42 | if (this._options.enable) { 43 | try { 44 | const routers = getCurrentPages().map( 45 | (route: { route: string; options: object }) => ({ 46 | route: route.route, 47 | options: route.options, 48 | }) 49 | ); 50 | 51 | return { 52 | ...event, 53 | extra: { 54 | ...event.extra, 55 | routers, 56 | }, 57 | }; 58 | } catch (e) { 59 | console.warn(`sentry-uniapp get router info fail: ${e}`); 60 | } 61 | } 62 | } 63 | 64 | return event; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sentry-uniapp", 3 | "version": "1.0.12", 4 | "description": "用于Uniapp/小程序/快应用等平台的 Sentry SDK", 5 | "repository": "git://github.com/uappkit/sentry-uniapp.git", 6 | "homepage": "https://github.com/uappkit/sentry-uniapp", 7 | "miniprogram": "dist", 8 | "main": "dist/index.js", 9 | "module": "esm/index.js", 10 | "types": "dist/index.d.ts", 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "build": "npm-run-all --parallel build:min build:esm build:ts version", 14 | "build:dist": "npm-run-all --parallel build:min", 15 | "build:min": "webpack --config ./webpack.config.min.js", 16 | "build:watch": "webpack --watch --config ./webpack.config.min.js", 17 | "build:ts": "tsc -p tsconfig.json", 18 | "build:esm": "tsc -p tsconfig.esm.json", 19 | "version": "node ./scripts/versionbump.js src/version.ts" 20 | }, 21 | "keywords": [ 22 | "sentry", 23 | "uniapp", 24 | "uniapp sentry" 25 | ], 26 | "author": "yinqisen@gmail.com", 27 | "license": "BSD-3-Clause", 28 | "engines": { 29 | "node": ">=14" 30 | }, 31 | "devDependencies": { 32 | "@sentry/typescript": "^5.20.0", 33 | "@types/node": "^12.7.1", 34 | "install": "^0.13.0", 35 | "miniprogram-api-typings": "^2.7.7-2", 36 | "npm": "^6.11.1", 37 | "npm-run-all": "^4.1.5", 38 | "replace-in-file": "^4.1.3", 39 | "ts-loader": "^6.0.4", 40 | "tslint": "^5.16.0", 41 | "typescript": "^3.5.3", 42 | "webpack": "^5.90.3", 43 | "webpack-cli": "^5.1.4", 44 | "webpack-merge": "^4.2.1" 45 | }, 46 | "dependencies": { 47 | "@sentry/core": "6.19.7", 48 | "@sentry/types": "6.19.7", 49 | "@sentry/utils": "6.19.7", 50 | "tslib": "^1.10.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /uapp-demo/uni.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里是uni-app内置的常用样式变量 3 | * 4 | * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 5 | * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App 6 | * 7 | */ 8 | 9 | /** 10 | * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 11 | * 12 | * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 13 | */ 14 | 15 | /* 颜色变量 */ 16 | 17 | /* 行为相关颜色 */ 18 | $uni-color-primary: #007aff; 19 | $uni-color-success: #4cd964; 20 | $uni-color-warning: #f0ad4e; 21 | $uni-color-error: #dd524d; 22 | 23 | /* 文字基本颜色 */ 24 | $uni-text-color:#333;//基本色 25 | $uni-text-color-inverse:#fff;//反色 26 | $uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 27 | $uni-text-color-placeholder: #808080; 28 | $uni-text-color-disable:#c0c0c0; 29 | 30 | /* 背景颜色 */ 31 | $uni-bg-color:#ffffff; 32 | $uni-bg-color-grey:#f8f8f8; 33 | $uni-bg-color-hover:#f1f1f1;//点击状态颜色 34 | $uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 35 | 36 | /* 边框颜色 */ 37 | $uni-border-color:#c8c7cc; 38 | 39 | /* 尺寸变量 */ 40 | 41 | /* 文字尺寸 */ 42 | $uni-font-size-sm:12px; 43 | $uni-font-size-base:14px; 44 | $uni-font-size-lg:16; 45 | 46 | /* 图片尺寸 */ 47 | $uni-img-size-sm:20px; 48 | $uni-img-size-base:26px; 49 | $uni-img-size-lg:40px; 50 | 51 | /* Border Radius */ 52 | $uni-border-radius-sm: 2px; 53 | $uni-border-radius-base: 3px; 54 | $uni-border-radius-lg: 6px; 55 | $uni-border-radius-circle: 50%; 56 | 57 | /* 水平间距 */ 58 | $uni-spacing-row-sm: 5px; 59 | $uni-spacing-row-base: 10px; 60 | $uni-spacing-row-lg: 15px; 61 | 62 | /* 垂直间距 */ 63 | $uni-spacing-col-sm: 4px; 64 | $uni-spacing-col-base: 8px; 65 | $uni-spacing-col-lg: 12px; 66 | 67 | /* 透明度 */ 68 | $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 69 | 70 | /* 文章场景相关 */ 71 | $uni-color-title: #2C405A; // 文章标题颜色 72 | $uni-font-size-title:20px; 73 | $uni-color-subtitle: #555555; // 二级标题颜色 74 | $uni-font-size-subtitle:26px; 75 | $uni-color-paragraph: #3F536E; // 文章段落颜色 76 | $uni-font-size-paragraph:15px; 77 | -------------------------------------------------------------------------------- /src/integrations/linkederrors.ts: -------------------------------------------------------------------------------- 1 | import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; 2 | import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; 3 | 4 | import { exceptionFromStacktrace } from '../parsers'; 5 | import { computeStackTrace } from '../tracekit'; 6 | 7 | const DEFAULT_KEY = 'cause'; 8 | const DEFAULT_LIMIT = 5; 9 | 10 | /** Adds SDK info to an event. */ 11 | export class LinkedErrors implements Integration { 12 | /** 13 | * @inheritDoc 14 | */ 15 | public readonly name: string = LinkedErrors.id; 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | public static id: string = 'LinkedErrors'; 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | private readonly _key: string; 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | private readonly _limit: number; 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public constructor(options: { key?: string; limit?: number } = {}) { 36 | this._key = options.key || DEFAULT_KEY; 37 | this._limit = options.limit || DEFAULT_LIMIT; 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public setupOnce(): void { 44 | addGlobalEventProcessor((event: Event, hint?: EventHint) => { 45 | const self = getCurrentHub().getIntegration(LinkedErrors); 46 | if (self) { 47 | return self._handler(event, hint); 48 | } 49 | return event; 50 | }); 51 | } 52 | 53 | /** 54 | * @inheritDoc 55 | */ 56 | private _handler(event: Event, hint?: EventHint): Event | null { 57 | if (!event.exception || !event.exception.values || !hint || !(hint.originalException instanceof Error)) { 58 | return event; 59 | } 60 | const linkedErrors = this._walkErrorTree(hint.originalException, this._key); 61 | event.exception.values = [...linkedErrors, ...event.exception.values]; 62 | return event; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | private _walkErrorTree(error: ExtendedError, key: string, stack: Exception[] = []): Exception[] { 69 | if (!(error[key] instanceof Error) || stack.length + 1 >= this._limit) { 70 | return stack; 71 | } 72 | const stacktrace = computeStackTrace(error[key]); 73 | const exception = exceptionFromStacktrace(stacktrace); 74 | return this._walkErrorTree(error[key], key, [exception, ...stack]); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient, Scope } from "@sentry/core"; 2 | import { DsnLike, Event, EventHint } from "@sentry/types"; 3 | 4 | import { MiniappBackend, MiniappOptions } from "./backend"; 5 | import { SDK_NAME, SDK_VERSION } from "./version"; 6 | 7 | /** 8 | * All properties the report dialog supports 9 | */ 10 | export interface ReportDialogOptions { 11 | [key: string]: any; 12 | eventId?: string; 13 | dsn?: DsnLike; 14 | user?: { 15 | email?: string; 16 | name?: string; 17 | }; 18 | lang?: string; 19 | title?: string; 20 | subtitle?: string; 21 | subtitle2?: string; 22 | labelName?: string; 23 | labelEmail?: string; 24 | labelComments?: string; 25 | labelClose?: string; 26 | labelSubmit?: string; 27 | errorGeneric?: string; 28 | errorFormEntry?: string; 29 | successMessage?: string; 30 | /** Callback after reportDialog showed up */ 31 | onLoad?(): void; 32 | } 33 | 34 | /** 35 | * The Sentry Miniapp SDK Client. 36 | * 37 | * @see MiniappOptions for documentation on configuration options. 38 | * @see SentryClient for usage documentation. 39 | */ 40 | export class MiniappClient extends BaseClient { 41 | /** 42 | * Creates a new Miniapp SDK instance. 43 | * 44 | * @param options Configuration options for this SDK. 45 | */ 46 | public constructor(options: MiniappOptions = {}) { 47 | super(MiniappBackend, options); 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike { 54 | event.platform = event.platform || "javascript"; 55 | event.sdk = { 56 | ...event.sdk, 57 | name: SDK_NAME, 58 | packages: [ 59 | ...((event.sdk && event.sdk.packages) || []), 60 | { 61 | name: "npm:sentry-uniapp", 62 | version: SDK_VERSION 63 | } 64 | ], 65 | version: SDK_VERSION 66 | }; 67 | 68 | return super._prepareEvent(event, scope, hint); 69 | } 70 | 71 | /** 72 | * Show a report dialog to the user to send feedback to a specific event. 73 | * 向用户显示报告对话框以将反馈发送到特定事件。---> 小程序上暂时用不到&不考虑。 74 | * 75 | * @param options Set individual options for the dialog 76 | */ 77 | public showReportDialog(options: ReportDialogOptions = {}): void { 78 | // doesn't work without a document (React Native) 79 | console.log('sentry-uniapp 暂未实现该方法', options); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/integrations/system.ts: -------------------------------------------------------------------------------- 1 | import { addGlobalEventProcessor, getCurrentHub } from "@sentry/core"; 2 | import { Event, Integration } from "@sentry/types"; 3 | 4 | import { appName as currentAppName, sdk } from "../crossPlatform"; 5 | import { SDK_VERSION } from "../version"; 6 | 7 | /** UserAgent */ 8 | export class System implements Integration { 9 | /** 10 | * @inheritDoc 11 | */ 12 | public name: string = System.id; 13 | 14 | /** 15 | * @inheritDoc 16 | */ 17 | public static id: string = "System"; 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | public setupOnce(): void { 23 | addGlobalEventProcessor((event: Event) => { 24 | if (getCurrentHub().getIntegration(System)) { 25 | try { 26 | const systemInfo = sdk.getSystemInfoSync() 27 | 28 | const { 29 | batteryLevel, // 微信小程序 30 | currentBattery, // 支付宝小程序、 钉钉小程序 31 | battery, // 字节跳动小程序 32 | brand, 33 | language, 34 | model, 35 | pixelRatio, 36 | platform, 37 | screenHeight, 38 | screenWidth, 39 | statusBarHeight, 40 | system, 41 | version, 42 | windowHeight, 43 | windowWidth, 44 | app, // 支付宝小程序 45 | appName, // 字节跳动小程序 46 | fontSizeSetting, // 支付宝小程序、 钉钉小程序、微信小程序 47 | } = systemInfo; 48 | 49 | // tslint:disable-next-line:variable-name 50 | const SDKVersion = SDK_VERSION; 51 | const [systemName, systemVersion] = system.split(" "); 52 | 53 | return { 54 | ...event, 55 | contexts: { 56 | ...event.contexts, 57 | device: { 58 | brand, 59 | battery_level: batteryLevel || currentBattery || battery, 60 | model, 61 | screen_dpi: pixelRatio 62 | }, 63 | os: { 64 | name: systemName || system, 65 | version: systemVersion || system 66 | }, 67 | extra: { 68 | SDKVersion, 69 | language, 70 | platform, 71 | screenHeight, 72 | screenWidth, 73 | statusBarHeight, 74 | version, 75 | windowHeight, 76 | windowWidth, 77 | fontSizeSetting, 78 | app: app || appName || currentAppName, 79 | ...systemInfo, 80 | } 81 | } 82 | }; 83 | } catch (e) { 84 | console.warn(`sentry-uniapp get system info fail: ${e}`); 85 | } 86 | } 87 | 88 | return event; 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/crossPlatform.ts: -------------------------------------------------------------------------------- 1 | declare const uni: any; // uniapp 2 | declare const wx: any; // 微信小程序、微信小游戏 3 | declare const my: any; // 支付宝小程序 4 | declare const tt: any; // 字节跳动小程序 5 | declare const dd: any; // 钉钉小程序 6 | declare const qq: any; // QQ 小程序、QQ 小游戏 7 | declare const swan: any; // 百度小程序 8 | 9 | /** 10 | * 小程序平台 SDK 接口 11 | */ 12 | interface SDK { 13 | request: Function; 14 | httpRequest?: Function; // 针对钉钉小程序 15 | getSystemInfo: Function; 16 | getSystemInfoSync: Function; 17 | onError?: Function; 18 | onUnhandledRejection?: Function; 19 | onPageNotFound?: Function; 20 | onMemoryWarning?: Function; 21 | getLaunchOptionsSync?: Function; 22 | } 23 | 24 | /** 25 | * 小程序平台 接口 26 | */ 27 | type AppName = 28 | | "uniapp" 29 | | "wechat" 30 | | "alipay" 31 | | "bytedance" 32 | | "dingtalk" 33 | | "qq" 34 | | "swan" 35 | | "quickapp" 36 | | "unknown"; 37 | 38 | let currentSdk: SDK = { 39 | // tslint:disable-next-line: no-empty 40 | request: () => { 41 | }, 42 | // tslint:disable-next-line: no-empty 43 | httpRequest: () => { 44 | }, 45 | // tslint:disable-next-line: no-empty 46 | getSystemInfoSync: () => { 47 | }, 48 | // tslint:disable-next-line: no-empty 49 | getSystemInfo: () => { 50 | }, 51 | }; 52 | 53 | /** 54 | * 获取跨平台的 SDK 55 | */ 56 | const getSDK = () => { 57 | if (typeof uni === "object") { 58 | currentSdk = uni; 59 | } else if (typeof wx === "object") { 60 | currentSdk = wx; 61 | } else if (typeof my === "object") { 62 | currentSdk = my; 63 | } else if (typeof tt === "object") { 64 | currentSdk = tt; 65 | } else if (typeof dd === "object") { 66 | currentSdk = dd; 67 | } else if (typeof qq === "object") { 68 | currentSdk = qq; 69 | } else if (typeof swan === "object") { 70 | currentSdk = swan; 71 | } else { 72 | // tslint:disable-next-line:no-console 73 | console.log("sentry-uniapp 暂不支持此平台, 快应用请使用 sentry-quickapp"); 74 | } 75 | 76 | return currentSdk; 77 | }; 78 | 79 | /** 80 | * 获取平台名称 81 | */ 82 | const getAppName = () => { 83 | let currentAppName: AppName = "unknown"; 84 | 85 | if (typeof uni === "object") { 86 | currentAppName = "uniapp"; 87 | } else if (typeof wx === "object") { 88 | currentAppName = "wechat"; 89 | } else if (typeof my === "object") { 90 | currentAppName = "alipay"; 91 | } else if (typeof tt === "object") { 92 | currentAppName = "bytedance"; 93 | } else if (typeof dd === "object") { 94 | currentAppName = "dingtalk"; 95 | } else if (typeof qq === "object") { 96 | currentAppName = "qq"; 97 | } else if (typeof swan === "object") { 98 | currentAppName = "swan"; 99 | } 100 | 101 | return currentAppName; 102 | }; 103 | 104 | const sdk = getSDK(); 105 | const appName = getAppName(); 106 | 107 | export {sdk, appName}; 108 | -------------------------------------------------------------------------------- /uapp-demo/README.md: -------------------------------------------------------------------------------- 1 | ## webapp 模版工程 2 | 3 | 模版里集成了 unocss/tailwindcss, uvui。 4 | 5 | 直接使用 tailwindcss 写样式,支持 `rpx` 单位,可参考 `pages/index/index.vue` 示例代码。 6 | 7 | ```bash 8 | # 先安装依赖库 9 | npm install 10 | ``` 11 | 12 | ### 1. 如何自定义配置 13 | 14 | 修改文件 `common/config.js` 15 | 16 | ```js 17 | // API_BASE_URL: API请求的URL域名前缀 18 | export const API_BASE_URL = process.env.NODE_ENV === 'development' 19 | ? 'https://api-dev.code0xff.com/' 20 | : 'https://api.code0xff.com/' 21 | 22 | // LOGIN_PAGE: 改为自己的登录页面,HTTP请求401时,会跳转到登录页 23 | export const LOGIN_PAGE = '/pages/login/login' 24 | 25 | // HOME_PAGE: 成功登录后的首页,或从登录导航栏跳转到首页 26 | export const HOME_PAGE = '/pages/index/index' 27 | 28 | // 使用 uni.setStorageSync(TOKEN_KEY) 保存用户的 token 29 | export const TOKEN_KEY = 'token' 30 | 31 | ``` 32 | 33 | ### 2. 如何直接写 class 样式: 34 | 35 | ```js 36 | 37 | 38 | 39 | ``` 40 | 41 | 42 | ### 3. 如何使用 @apply 定义 class: 43 | 44 | ```vue 45 | 46 | 47 | ``` 48 | 49 | ```vue 50 | 51 | 52 | 53 | .btn-item { 54 | @apply w-100rpx h-100rpx rounded-full overflow-hidden mx-30rpx; 55 | } 56 | ``` 57 | 58 | ### 4. 如何直接用 uni.$uv.http 网络请求: 59 | 60 | > 注意 GET 方法和 POST等其他方法,从第二个参数开始的区别 61 | 62 | **GET 方法** 63 | 64 | ```js 65 | // 用uni.$uv.queryParams 拼接URL字串 66 | uni.$uv.http.get("/api/categories" + uni.$uv.queryParams(params)) 67 | 68 | // 直接内嵌变量 69 | uni.$uv.http.get(`api/data/list?page=${page}&perPage=${perPage}`) 70 | ``` 71 | 72 | **POST 方法** 73 | 74 | ```js 75 | // 直接使用 post 76 | uni.$uv.http.post('/api/auth/apple/oaut', { 77 | info: loginRes.appleInfo, 78 | }).then(res => { 79 | // ... 80 | }) 81 | 82 | // POST 方法,注意第三个参数 custom.catch = true, 避免异常被 http 封装拦截 83 | uni.$uv.http.post('/api/auth/apple/oauth', { 84 | info: loginRes.appleInfo, 85 | }, { 86 | custom: { catch: true }, 87 | }).then(res => { 88 | // ... 89 | }).catch((e) => { 90 | // catch exception 91 | }) 92 | ``` 93 | 94 | **上传 upload 用法** 95 | 96 | ```js 97 | uni.$uv.http.upload('/api/video/mixcut', { 98 | name: this.fileList[0].name, 99 | filePath: this.fileList[0].url, 100 | formData: this.FormData 101 | }).then(res => { 102 | this.fileList = [] 103 | uni.navigateBack() 104 | }).catch(() => { 105 | uni.$uv.toast('提交失败!') 106 | }) 107 | ``` 108 | 109 | **如何集成 sentry** 110 | 111 | `npm i sentry-uniapp` 112 | 113 | 114 | 115 | ### 5. 参考 116 | 117 | uapp 文档,方便 uni-app 跨平台快速开发的 cli 118 | 119 | 120 | tailwindcss 文档 121 | 122 | 123 | uvui (基于uview,支持 vue3) 124 | 125 | -------------------------------------------------------------------------------- /src/backend.ts: -------------------------------------------------------------------------------- 1 | import { BaseBackend } from "@sentry/core"; 2 | import { Event, EventHint, Options, Severity, Transport } from "@sentry/types"; 3 | import { addExceptionMechanism, resolvedSyncPromise } from '@sentry/utils'; 4 | 5 | import { eventFromString, eventFromUnknownInput } from './eventbuilder'; 6 | import { XHRTransport } from "./transports/index"; 7 | 8 | /** 9 | * Configuration options for the Sentry Miniapp SDK. 10 | * Sentry Miniapp SDK 的配置选项。 11 | * @see MiniappClient for more information. 12 | */ 13 | export interface MiniappOptions extends Options { 14 | /** 15 | * A pattern for error URLs which should not be sent to Sentry. 16 | * To whitelist certain errors instead, use {@link Options.whitelistUrls}. 17 | * By default, all errors will be sent. 18 | */ 19 | blacklistUrls?: Array; 20 | 21 | /** 22 | * A pattern for error URLs which should exclusively be sent to Sentry. 23 | * This is the opposite of {@link Options.blacklistUrls}. 24 | * By default, all errors will be sent. 25 | */ 26 | whitelistUrls?: Array; 27 | 28 | extraOptions?: Object; 29 | } 30 | 31 | /** 32 | * The Sentry Browser SDK Backend. 33 | * @hidden 34 | */ 35 | export class MiniappBackend extends BaseBackend { 36 | /** 37 | * @inheritDoc 38 | */ 39 | protected _setupTransport(): Transport { 40 | if (!this._options.dsn) { 41 | // We return the noop transport here in case there is no Dsn. 42 | return super._setupTransport(); 43 | } 44 | 45 | const transportOptions = { 46 | ...this._options.transportOptions, 47 | dsn: this._options.dsn 48 | }; 49 | 50 | if (this._options.transport) { 51 | return new this._options.transport(transportOptions); 52 | } 53 | 54 | return new XHRTransport(transportOptions); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public eventFromException(exception: any, hint?: EventHint): PromiseLike { 61 | const syntheticException = (hint && hint.syntheticException) || undefined; 62 | const event = eventFromUnknownInput(exception, syntheticException, { 63 | attachStacktrace: this._options.attachStacktrace, 64 | }); 65 | addExceptionMechanism(event, { 66 | handled: true, 67 | type: 'generic', 68 | }); 69 | event.level = Severity.Error; 70 | if (hint && hint.event_id) { 71 | event.event_id = hint.event_id; 72 | } 73 | return resolvedSyncPromise(event); 74 | } 75 | /** 76 | * @inheritDoc 77 | */ 78 | public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { 79 | const syntheticException = (hint && hint.syntheticException) || undefined; 80 | const event = eventFromString(message, syntheticException, { 81 | attachStacktrace: this._options.attachStacktrace, 82 | }); 83 | event.level = level; 84 | if (hint && hint.event_id) { 85 | event.event_id = hint.event_id; 86 | } 87 | return resolvedSyncPromise(event); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /uapp-demo/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "uapp-demo", 3 | "appid" : "__UNI__61AF962", 4 | "description" : "made by uapp", 5 | "versionName" : "1.0.0", 6 | "versionCode" : "100", 7 | "transformPx" : false, 8 | "uapp": { 9 | "name": "uapp", 10 | "package": "com.code0xff.uapp", 11 | "android.appkey": "申请并替换为 android dcloudkey", 12 | "ios.appkey": "申请并替换为 ios dcloudkey" 13 | }, 14 | /* 5+App特有相关 */ 15 | "app-plus" : { 16 | "usingComponents" : true, 17 | "nvueStyleCompiler" : "uni-app", 18 | "compilerVersion" : 3, 19 | "splashscreen" : { 20 | "alwaysShowBeforeRender" : true, 21 | "waiting" : true, 22 | "autoclose" : true, 23 | "delay" : 0 24 | }, 25 | /* 模块配置 */ 26 | "modules" : {}, 27 | /* 应用发布信息 */ 28 | "distribute" : { 29 | /* android打包配置 */ 30 | "android" : { 31 | "permissions" : [ 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "", 41 | "", 42 | "", 43 | "", 44 | "", 45 | "", 46 | "" 47 | ] 48 | }, 49 | /* ios打包配置 */ 50 | "ios" : {}, 51 | /* SDK配置 */ 52 | "sdkConfigs" : {} 53 | } 54 | }, 55 | /* 快应用特有相关 */ 56 | "quickapp" : {}, 57 | /* 小程序特有相关 */ 58 | "mp-weixin" : { 59 | "appid" : "", 60 | "setting" : { 61 | "urlCheck" : false 62 | }, 63 | "usingComponents" : true 64 | }, 65 | "mp-alipay" : { 66 | "usingComponents" : true 67 | }, 68 | "mp-baidu" : { 69 | "usingComponents" : true 70 | }, 71 | "mp-toutiao" : { 72 | "usingComponents" : true 73 | }, 74 | "uniStatistics" : { 75 | "enable" : false 76 | }, 77 | "vueVersion" : "3" 78 | } 79 | -------------------------------------------------------------------------------- /uapp-demo/common/http/index.js: -------------------------------------------------------------------------------- 1 | // 此vm参数为页面的实例,可以通过它引用vuex中的变量 2 | import { goLoginPage } from '@/common/utils/login' 3 | import { API_BASE_URL, TOKEN_KEY } from '@/common/config' 4 | 5 | export const Request = (vm) => { 6 | // 初始化请求配置 7 | uni.$uv.http.setConfig((config) => { 8 | config = { 9 | baseURL: API_BASE_URL, 10 | dataType: 'json', 11 | // #ifndef MP-ALIPAY 12 | responseType: 'text', 13 | // #endif 14 | // 注:如果局部custom与全局custom有同名属性,则后面的属性会覆盖前面的属性,相当于Object.assign(全局,局部) 15 | custom: {}, // 全局自定义参数默认值 16 | 17 | header: { 18 | Accept: 'application/json', 19 | // #ifdef H5 || ELECTRON 20 | appid: 'wx-web-appid', 21 | // #endif 22 | // #ifndef H5 || ELECTRON 23 | Referer: 'https://code0xff.com/wx-app-appid/app/page-frame.html', 24 | // #endif 25 | }, 26 | 27 | // #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN 28 | timeout: 600000, 29 | // #endif 30 | // #ifdef APP-PLUS 31 | sslVerify: true, 32 | // #endif 33 | // #ifdef H5 34 | // 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+) 35 | withCredentials: false, 36 | // #endif 37 | // #ifdef APP-PLUS 38 | firstIpv4: false, // DNS解析时优先使用ipv4 仅 App-Android 支持 (HBuilderX 2.8.0+) 39 | // #endif 40 | } 41 | 42 | return config 43 | }) 44 | 45 | uni.$uv.http.getHeaderWithToken = (config = null) => { 46 | if (!config) { 47 | config = uni.$uv.http.config 48 | } 49 | 50 | let token = uni.getStorageSync(TOKEN_KEY) 51 | if (token) { 52 | config.header.Authorization = `Bearer ${token}` 53 | } 54 | return config.header 55 | } 56 | 57 | // 请求拦截 58 | uni.$uv.http.interceptors.request.use((config) => { // 可使用async await 做异步操作 59 | // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{} 60 | config.data = config.data || {} 61 | uni.$uv.http.getHeaderWithToken(config) 62 | return config 63 | }, config => { // 可使用async await 做异步操作 64 | return Promise.reject(config) 65 | }) 66 | 67 | // 响应拦截 68 | uni.$uv.http.interceptors.response.use((response) => { 69 | // 对响应成功做点什么 可使用async await 做异步操作 70 | const data = response.data 71 | 72 | if (response.statusCode === 429) { 73 | uni.$uv.toast('您的操作过于频繁,请稍后再试') 74 | return Promise.reject('您的操作过于频繁,请稍后再试') 75 | } 76 | 77 | if (response.statusCode === 401) { 78 | // token过期 79 | uni.removeStorageSync(TOKEN_KEY) 80 | goLoginPage() 81 | return new Promise(() => {}) 82 | } 83 | 84 | // 自定义参数 85 | const custom = response.config?.custom 86 | if (response.statusCode >= 400) { 87 | // getApp().globalData.sentry.captureMessage(data.message, data) 88 | 89 | // 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示 90 | if (custom.toast !== false) { 91 | uni.$uv.toast(data.message) 92 | } 93 | 94 | // 如果需要catch返回,则进行reject 95 | if (custom?.catch) { 96 | return Promise.reject(data) 97 | } else { 98 | // 否则返回一个pending中的promise,请求不会进入catch中 99 | return new Promise(() => {}) 100 | } 101 | } 102 | 103 | return custom?.raw ? response : data 104 | }, (response) => { 105 | // 对响应错误做点什么 (statusCode !== 200) 106 | return Promise.reject(response) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /src/parsers.ts: -------------------------------------------------------------------------------- 1 | import { Event, Exception, StackFrame } from '@sentry/types'; 2 | import { extractExceptionKeysForMessage, isEvent, normalizeToSize } from '@sentry/utils'; 3 | 4 | import { computeStackTrace, StackFrame as TraceKitStackFrame, StackTrace as TraceKitStackTrace } from './tracekit'; 5 | 6 | const STACKTRACE_LIMIT = 100; 7 | 8 | /** 9 | * This function creates an exception from an TraceKitStackTrace 10 | * @param stacktrace TraceKitStackTrace that will be converted to an exception 11 | * @hidden 12 | */ 13 | export function exceptionFromStacktrace(stacktrace: TraceKitStackTrace): Exception { 14 | const frames = prepareFramesForEvent(stacktrace.stack); 15 | 16 | const exception: Exception = { 17 | type: stacktrace.name, 18 | value: stacktrace.message, 19 | }; 20 | 21 | if (frames && frames.length) { 22 | exception.stacktrace = { frames }; 23 | } 24 | 25 | // tslint:disable-next-line:strict-type-predicates 26 | if (exception.type === undefined && exception.value === '') { 27 | exception.value = 'Unrecoverable error caught'; 28 | } 29 | 30 | return exception; 31 | } 32 | 33 | /** 34 | * @hidden 35 | */ 36 | export function eventFromPlainObject(exception: {}, syntheticException?: Error, rejection?: boolean): Event { 37 | const event: Event = { 38 | exception: { 39 | values: [ 40 | { 41 | type: isEvent(exception) ? exception.constructor.name : rejection ? 'UnhandledRejection' : 'Error', 42 | value: `Non-Error ${rejection ? 'promise rejection' : 'exception' 43 | } captured with keys: ${extractExceptionKeysForMessage(exception)}`, 44 | }, 45 | ], 46 | }, 47 | extra: { 48 | __serialized__: normalizeToSize(exception), 49 | }, 50 | }; 51 | 52 | if (syntheticException) { 53 | const stacktrace = computeStackTrace(syntheticException); 54 | const frames = prepareFramesForEvent(stacktrace.stack); 55 | event.stacktrace = { 56 | frames, 57 | }; 58 | } 59 | 60 | return event; 61 | } 62 | 63 | /** 64 | * @hidden 65 | */ 66 | export function eventFromStacktrace(stacktrace: TraceKitStackTrace): Event { 67 | const exception = exceptionFromStacktrace(stacktrace); 68 | 69 | return { 70 | exception: { 71 | values: [exception], 72 | }, 73 | }; 74 | } 75 | 76 | /** 77 | * @hidden 78 | */ 79 | export function prepareFramesForEvent(stack: TraceKitStackFrame[]): StackFrame[] { 80 | if (!stack || !stack.length) { 81 | return []; 82 | } 83 | 84 | let localStack = stack; 85 | 86 | const firstFrameFunction = localStack[0].func || ''; 87 | const lastFrameFunction = localStack[localStack.length - 1].func || ''; 88 | 89 | // If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call) 90 | if (firstFrameFunction.indexOf('captureMessage') !== -1 || firstFrameFunction.indexOf('captureException') !== -1) { 91 | localStack = localStack.slice(1); 92 | } 93 | 94 | // If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call) 95 | if (lastFrameFunction.indexOf('sentryWrapped') !== -1) { 96 | localStack = localStack.slice(0, -1); 97 | } 98 | 99 | // The frame where the crash happened, should be the last entry in the array 100 | return localStack 101 | .map( 102 | (frame: TraceKitStackFrame): StackFrame => ({ 103 | colno: frame.column === null ? undefined : frame.column, 104 | filename: frame.url || localStack[0].url, 105 | function: frame.func || '?', 106 | in_app: true, 107 | lineno: frame.line === null ? undefined : frame.line, 108 | }), 109 | ) 110 | .slice(0, STACKTRACE_LIMIT) 111 | .reverse(); 112 | } 113 | -------------------------------------------------------------------------------- /src/eventbuilder.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@sentry/types'; 2 | import { 3 | addExceptionMechanism, 4 | addExceptionTypeValue, 5 | isDOMError, 6 | isDOMException, 7 | isError, 8 | isErrorEvent, 9 | isEvent, 10 | isPlainObject, 11 | } from '@sentry/utils'; 12 | 13 | import { eventFromPlainObject, eventFromStacktrace, prepareFramesForEvent } from './parsers'; 14 | import { computeStackTrace } from './tracekit'; 15 | 16 | /** JSDoc */ 17 | export function eventFromUnknownInput( 18 | exception: unknown, 19 | syntheticException?: Error, 20 | options: { 21 | rejection?: boolean; 22 | attachStacktrace?: boolean; 23 | } = {}, 24 | ): Event { 25 | let event: Event; 26 | 27 | if (isErrorEvent(exception as ErrorEvent) && (exception as ErrorEvent).error) { 28 | // If it is an ErrorEvent with `error` property, extract it to get actual Error 29 | const errorEvent = exception as ErrorEvent; 30 | exception = errorEvent.error; // tslint:disable-line:no-parameter-reassignment 31 | event = eventFromStacktrace(computeStackTrace(exception as Error)); 32 | return event; 33 | } 34 | if (isDOMError(exception as DOMError) || isDOMException(exception as DOMException)) { 35 | // If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers) 36 | // then we just extract the name and message, as they don't provide anything else 37 | // https://developer.mozilla.org/en-US/docs/Web/API/DOMError 38 | // https://developer.mozilla.org/en-US/docs/Web/API/DOMException 39 | const domException = exception as DOMException; 40 | const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException'); 41 | const message = domException.message ? `${name}: ${domException.message}` : name; 42 | 43 | event = eventFromString(message, syntheticException, options); 44 | addExceptionTypeValue(event, message); 45 | return event; 46 | } 47 | if (isError(exception as Error)) { 48 | // we have a real Error object, do nothing 49 | event = eventFromStacktrace(computeStackTrace(exception as Error)); 50 | return event; 51 | } 52 | if (isPlainObject(exception) || isEvent(exception)) { 53 | // If it is plain Object or Event, serialize it manually and extract options 54 | // This will allow us to group events based on top-level keys 55 | // which is much better than creating new group when any key/value change 56 | const objectException = exception as {}; 57 | event = eventFromPlainObject(objectException, syntheticException, options.rejection); 58 | addExceptionMechanism(event, { 59 | synthetic: true, 60 | }); 61 | return event; 62 | } 63 | 64 | // If none of previous checks were valid, then it means that it's not: 65 | // - an instance of DOMError 66 | // - an instance of DOMException 67 | // - an instance of Event 68 | // - an instance of Error 69 | // - a valid ErrorEvent (one with an error property) 70 | // - a plain Object 71 | // 72 | // So bail out and capture it as a simple message: 73 | event = eventFromString(exception as string, syntheticException, options); 74 | addExceptionTypeValue(event, `${exception}`, undefined); 75 | addExceptionMechanism(event, { 76 | synthetic: true, 77 | }); 78 | 79 | return event; 80 | } 81 | 82 | // this._options.attachStacktrace 83 | /** JSDoc */ 84 | export function eventFromString( 85 | input: string, 86 | syntheticException?: Error, 87 | options: { 88 | attachStacktrace?: boolean; 89 | } = {}, 90 | ): Event { 91 | const event: Event = { 92 | message: input, 93 | }; 94 | 95 | if (options.attachStacktrace && syntheticException) { 96 | const stacktrace = computeStackTrace(syntheticException); 97 | const frames = prepareFramesForEvent(stacktrace.stack); 98 | event.stacktrace = { 99 | frames, 100 | }; 101 | } 102 | 103 | return event; 104 | } 105 | -------------------------------------------------------------------------------- /src/sdk.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getCurrentHub, 3 | initAndBind, 4 | Integrations as CoreIntegrations, 5 | } from "@sentry/core"; 6 | import { resolvedSyncPromise } from "@sentry/utils"; 7 | 8 | import { MiniappOptions } from "./backend"; 9 | import { MiniappClient, ReportDialogOptions } from "./client"; 10 | import { wrap as internalWrap } from "./helpers"; 11 | import { 12 | GlobalHandlers, 13 | IgnoreMpcrawlerErrors, 14 | LinkedErrors, 15 | Router, 16 | System, 17 | TryCatch, 18 | } from "./integrations/index"; 19 | 20 | export const defaultIntegrations = [ 21 | new CoreIntegrations.InboundFilters(), 22 | new CoreIntegrations.FunctionToString(), 23 | new TryCatch(), 24 | new GlobalHandlers(), 25 | new LinkedErrors(), 26 | 27 | new System(), 28 | new Router(), 29 | new IgnoreMpcrawlerErrors(), 30 | ]; 31 | 32 | /** 33 | * The Sentry Uniapp SDK Client. 34 | * 35 | * To use this SDK, call the {@link init} function as early as possible when 36 | * launching the app. To set context information or send manual events, use 37 | * the provided methods. 38 | * 39 | * @example 40 | * ``` 41 | * import { init } from 'sentry-uniapp'; 42 | * 43 | * init({ 44 | * dsn: '__DSN__', 45 | * // ... 46 | * }); 47 | * ``` 48 | * 49 | * @example 50 | * ``` 51 | * import { configureScope } from 'sentry-uniapp'; 52 | * 53 | * configureScope((scope: Scope) => { 54 | * scope.setExtra({ battery: 0.7 }); 55 | * scope.setTag({ user_mode: 'admin' }); 56 | * scope.setUser({ id: '4711' }); 57 | * }); 58 | * ``` 59 | * 60 | * @example 61 | * ``` 62 | * import { addBreadcrumb } from 'sentry-uniapp'; 63 | * 64 | * addBreadcrumb({ 65 | * message: 'My Breadcrumb', 66 | * // ... 67 | * }); 68 | * ``` 69 | * 70 | * @example 71 | * ``` 72 | * import * as Sentry from 'sentry-uniapp'; 73 | * 74 | * Sentry.captureMessage('Hello, world!'); 75 | * Sentry.captureException(new Error('Good bye')); 76 | * Sentry.captureEvent({ 77 | * message: 'Manual', 78 | * stacktrace: [ 79 | * // ... 80 | * ], 81 | * }); 82 | * ``` 83 | * 84 | * @see {@link MiniappOptions} for documentation on configuration options. 85 | */ 86 | export function init(options: MiniappOptions = {}): void { 87 | // 如果将 options.defaultIntegrations 设置为 false,则不会添加默认集成,否则将在内部将其设置为建议的默认集成。 88 | // tslint:disable-next-line: strict-comparisons 89 | if (options.defaultIntegrations === undefined) { 90 | options.defaultIntegrations = defaultIntegrations; 91 | } 92 | 93 | options.normalizeDepth = options.normalizeDepth || 5; 94 | if (options.defaultIntegrations) { 95 | (options.defaultIntegrations[3] as GlobalHandlers).setExtraOptions(options.extraOptions); 96 | } 97 | 98 | initAndBind(MiniappClient, options); 99 | } 100 | 101 | /** 102 | * Present the user with a report dialog. 103 | * 向用户显示报告对话框。小程序上暂时不考虑实现该功能。 104 | * 105 | * @param options Everything is optional, we try to fetch all info need from the global scope. 106 | */ 107 | export function showReportDialog(options: ReportDialogOptions = {}): void { 108 | if (!options.eventId) { 109 | options.eventId = getCurrentHub().lastEventId(); 110 | } 111 | const client = getCurrentHub().getClient(); 112 | if (client) { 113 | client.showReportDialog(options); 114 | } 115 | } 116 | 117 | /** 118 | * This is the getter for lastEventId. 获取 lastEventId。 119 | * 120 | * @returns The last event id of a captured event. 121 | */ 122 | export function lastEventId(): string | undefined { 123 | return getCurrentHub().lastEventId(); 124 | } 125 | 126 | /** 127 | * A promise that resolves when all current events have been sent. 128 | * If you provide a timeout and the queue takes longer to drain the promise returns false. 129 | * 在发送所有当前事件时会变为 resolved 状态的 promise。如果提供了一个超时时间并且队列需要更长时间来消耗,则 promise 将返回 false。 130 | * 131 | * @param timeout Maximum time in ms the client should wait. 132 | */ 133 | export function flush(timeout?: number): PromiseLike { 134 | const client = getCurrentHub().getClient(); 135 | if (client) { 136 | return client.flush(timeout); 137 | } 138 | return resolvedSyncPromise(false); 139 | } 140 | 141 | /** 142 | * A promise that resolves when all current events have been sent. 143 | * If you provide a timeout and the queue takes longer to drain the promise returns false. 144 | * 145 | * @param timeout Maximum time in ms the client should wait. 146 | */ 147 | export function close(timeout?: number): PromiseLike { 148 | const client = getCurrentHub().getClient(); 149 | if (client) { 150 | return client.close(timeout); 151 | } 152 | return resolvedSyncPromise(false); 153 | } 154 | 155 | /** 156 | * Wrap code within a try/catch block so the SDK is able to capture errors. 157 | * 在 try / catch 块中包装代码,以便 SDK 能够捕获错误。 158 | * 实际上是 ./helpers 文件中 warp 方法的进一步封装。 159 | * 160 | * @param fn A function to wrap. 161 | * 162 | * @returns The result of wrapped function call. 163 | */ 164 | export function wrap(fn: Function): any { 165 | // tslint:disable-next-line: no-unsafe-any 166 | return internalWrap(fn)(); 167 | } 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sentry Uniapp SDK 2 | 3 | `sentry Uniapp SDK` 的封装,可用于 Uniapp 全端,包含app, h5,微信小程序,抖音小程序,百度小程序等各家平台。 4 | 5 | 同时支持 Uniapp vue2 / vue3 工程。 6 | 7 | > 提示:由于快应用 require 方式特殊性,webpack是在编译期处理的,动态代码检测无效,所以单独维护,包名为 sentry-quickapp。 8 | 9 | 快应用项目可参考: 10 | 11 | 12 | 13 | ## 用法 14 | 15 | 1、安装依赖 16 | 17 | ```bash 18 | npm install sentry-uniapp 19 | ``` 20 | 21 | 2、在 `App.vue => onLaunch` 中初始化 22 | 23 | 注意替换下方代码里的 `__DSN__`,并仔细阅读注释说明和常见问题 24 | 25 | ```js 26 | export default { 27 | onLaunch: function () { 28 | console.log('App Launch'); 29 | sentry.init({ 30 | // __DSN__ 参考格式: https://8137b89b2d1c4e349da3a38dca80c5fe@sentry.io/1 31 | dsn: '__DSN__', 32 | 33 | // extraOptions 主要是解决平台差异问题的,见下方说明 34 | // 非 APP 平台,可以不用 35 | extraOptions: { onmemorywarning: false, onerror: false } 36 | }); 37 | 38 | // 代码上报,extra 为可选的自定义对象内容 39 | sentry.captureMessage('custom message from ' + uni.getSystemInfoSync().platform, { 40 | UserId: 123, 41 | Command: 'npm i -g uapp' 42 | }); 43 | 44 | // 触发一个未定义函数的错误 45 | balabala(); 46 | }, 47 | 48 | // sentry-uniapp 内部是通过 uni.onError 钩子函数捕获错误的 49 | // 但目前 uni.onError 暂不支持 App (android / ios),各平台支持情况参考: 50 | // https://uniapp.dcloud.net.cn/api/application.html#onerror 51 | // 52 | // 通用方案: 53 | // 可用 App.onError 自己处理,但需要先禁用 sentry 里的捕获 54 | // 方法在 sentry.init 参数里加上 extraOptions: { onerror: false } 55 | onError: function (e) { 56 | sentry.captureException(e); 57 | } 58 | } 59 | ``` 60 | 61 | 3、其他可选配置 62 | 63 | ```js 64 | // Set user information, as well as tags and further extras 65 | sentry.configureScope((scope) => { 66 | scope.setExtra("battery", 0.7); 67 | scope.setTag("user_mode", "admin"); 68 | scope.setUser({ id: "4711" }); 69 | // scope.clear(); 70 | }); 71 | 72 | // Add a breadcrumb for future events 73 | sentry.addBreadcrumb({ 74 | message: "My Breadcrumb", 75 | // ... 76 | }); 77 | 78 | // Capture exceptions, messages or manual events 79 | // Error 无法定义标题,可以用下面的 captureMessage 80 | sentry.captureException(new Error("Good bye")); 81 | 82 | // captureMessage 可以定制消息标题,extra 为附加的对象内容 83 | sentry.captureMessage("message title", { 84 | extra 85 | }); 86 | 87 | sentry.captureEvent({ 88 | message: "Manual", 89 | stacktrace: [ 90 | // ... 91 | ], 92 | }); 93 | ``` 94 | 95 | ## 参考示例 96 | 97 | 项目代码里的 `uapp-demo`,通过 HBuilderX 打开即可,下面截图为 `uapp-demo` 在各平台测试结果。 98 | 99 | ![pass](./assets/sentry-screetshot.png) 100 | 101 | ## 常见问题 102 | 103 | 1、大多数本地环境问题,成功安装后,目录结构如下图: 104 | 105 | ![project](./assets/project.png) 106 | 107 | 先通过 sentry.captureMessage 确认能上报成功,如果收不到, 108 | 109 | * 可检测 `__DSN__` 是否正确 110 | * 检测网络,最好通过配置代理拦截下网络请求是否存在 111 | 112 | 2、提示 onmemorywarning 未实现错误 113 | 114 | > API `onMemoryWarning` is not yet implemented __ERROR 115 | 116 | 有的平台不支持 `memorywarning` 的监听,可以 sentry.init 里禁用: 117 | 118 | ```.js 119 | sentry.init({ 120 | dsn: '__DSN__', 121 | extraOptions: { onmemorywarning: false } 122 | }); 123 | ``` 124 | 125 | 3、暂不支持 sentry.init 开启 debug,移除 或设置 false 126 | 127 | > [Vue warn]: Error in onLaunch hook: "TypeError: undefined is not an object (evaluating '(_a = global.console)[name]')"[ERROR] : [Vue warn]: Error in onLaunch hook: "TypeError: undefined is not an object (evaluating '(_a = global.console)[name]')"(found at App.vue:1) __ERROR 128 | 129 | ```.js 130 | sentry.init({ 131 | dsn: '__DSN__', 132 | debug: false, 133 | }); 134 | ``` 135 | 136 | 4、代码异常没有自动上报的,可查看 HBuilderX 的 log 窗口,区分以下两种错误情况 137 | 138 | [JS Framework] 开头,由 framewrok 底层拦截 `不会触发 sentry 上报`,错误信息如下: 139 | 140 | > [JS Framework] Failed to execute the callback function:[ERROR] : [JS Framework] Failed to execute the callback function:ReferenceError: Can't find variable: balabala __ERROR 141 | 142 | Vue 层报的错误,可以触发 sentry 上报,错误信息如下: 143 | 144 | > [Vue warn]: Error in onLaunch hook: "ReferenceError: Can't find variable: balabala"[ERROR] : [Vue warn]: Error in onLaunch hook: "ReferenceError: Can't find variable: balabala"(found at App.vue:1) __ERROR 145 | 146 | ## 功能特点 147 | 148 | - [x] 基于 [sentry-javascript 最新的基础模块](https://www.yuque.com/lizhiyao/dxy/zevhf1#0GMCN) 封装 149 | - [x] 遵守[官方统一的 API 设计文档](https://www.yuque.com/lizhiyao/dxy/gc3b9r#vQdTs),使用方式和官方保持一致 150 | - [x] 使用 [TypeScript](https://www.typescriptlang.org/) 进行编写 151 | - [x] 包含 Sentry SDK(如:[@sentry/browser](https://github.com/getsentry/sentry-javascript/tree/master/packages/browser))的所有基础功能 152 | - [x] 支持 `ES6`、`CommonJS` 两种模块系统(支持小程序原生开发方式、使用小程序框架开发方式两种开发模式下使用) 153 | - [x] 默认监听并上报小程序的 onError、onUnhandledRejection、onPageNotFound、onMemoryWarning 事件返回的信息(各事件支持程度与对应各小程序官方保持一致) 154 | - [x] 默认上报运行小程序的设备、操作系统、应用版本信息 155 | - [x] 支持微信小程序 156 | - [x] 支持微信小游戏 157 | - [x] 支持字节跳动小程序 158 | - [x] 支持支付宝小程序 159 | - [x] 支持钉钉小程序 160 | - [x] 支持百度小程序 161 | - [x] 支持快应用 162 | - [x] 支持在 [Taro](https://taro.aotu.io/) 等第三方小程序框架中使用 163 | - [x] 默认上报异常发生时的路由栈 164 | - [ ] 完善的代码测试 165 | 166 | ## 感谢 167 | 168 | 本项目基于下面开源基础上修改,感谢原作者: 169 | 170 | 171 | 172 | ## 联系作者 173 | 174 | 微信: yinqisen 175 | 176 | 推荐另一个开源作品 `uapp`, 方便 Uniapp 离线打包的 cli。 177 | 178 | 179 | 180 | ![uapp](./uapp-demo/static/logo.png) 181 | -------------------------------------------------------------------------------- /src/integrations/trycatch.ts: -------------------------------------------------------------------------------- 1 | import { Integration, WrappedFunction } from "@sentry/types"; 2 | import { fill, getGlobalObject } from "@sentry/utils"; 3 | 4 | import { wrap } from "../helpers"; 5 | 6 | /** Wrap timer functions and event targets to catch errors and provide better meta data */ 7 | export class TryCatch implements Integration { 8 | /** JSDoc */ 9 | private _ignoreOnError: number = 0; 10 | 11 | /** 12 | * @inheritDoc 13 | */ 14 | public name: string = TryCatch.id; 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | public static id: string = "TryCatch"; 20 | 21 | /** JSDoc */ 22 | private _wrapTimeFunction(original: () => void): () => number { 23 | return function(this: any, ...args: any[]): number { 24 | const originalCallback = args[0]; 25 | args[0] = wrap(originalCallback, { 26 | mechanism: { 27 | data: { function: getFunctionName(original) }, 28 | handled: true, 29 | type: "instrument" 30 | } 31 | }); 32 | return original.apply(this, args); 33 | }; 34 | } 35 | 36 | /** JSDoc */ 37 | private _wrapRAF(original: any): (callback: () => void) => any { 38 | return function(this: any, callback: () => void): () => void { 39 | return original( 40 | wrap(callback, { 41 | mechanism: { 42 | data: { 43 | function: "requestAnimationFrame", 44 | handler: getFunctionName(original) 45 | }, 46 | handled: true, 47 | type: "instrument" 48 | } 49 | }) 50 | ); 51 | }; 52 | } 53 | 54 | /** JSDoc */ 55 | private _wrapEventTarget(target: string): void { 56 | const global = getGlobalObject() as { [key: string]: any }; 57 | const proto = global[target] && global[target].prototype; 58 | 59 | if ( 60 | !proto || 61 | !proto.hasOwnProperty || 62 | !proto.hasOwnProperty("addEventListener") 63 | ) { 64 | return; 65 | } 66 | 67 | fill(proto, "addEventListener", function( 68 | original: () => void 69 | ): ( 70 | eventName: string, 71 | fn: EventListenerObject, 72 | options?: boolean | AddEventListenerOptions 73 | ) => void { 74 | return function( 75 | this: any, 76 | eventName: string, 77 | fn: EventListenerObject, 78 | options?: boolean | AddEventListenerOptions 79 | ): ( 80 | eventName: string, 81 | fn: EventListenerObject, 82 | capture?: boolean, 83 | secure?: boolean 84 | ) => void { 85 | try { 86 | // tslint:disable-next-line:no-unbound-method strict-type-predicates 87 | if (typeof fn.handleEvent === "function") { 88 | fn.handleEvent = wrap(fn.handleEvent.bind(fn), { 89 | mechanism: { 90 | data: { 91 | function: "handleEvent", 92 | handler: getFunctionName(fn), 93 | target 94 | }, 95 | handled: true, 96 | type: "instrument" 97 | } 98 | }); 99 | } 100 | } catch (err) { 101 | // can sometimes get 'Permission denied to access property "handle Event' 102 | } 103 | 104 | return original.call( 105 | this, 106 | eventName, 107 | wrap((fn as any) as WrappedFunction, { 108 | mechanism: { 109 | data: { 110 | function: "addEventListener", 111 | handler: getFunctionName(fn), 112 | target 113 | }, 114 | handled: true, 115 | type: "instrument" 116 | } 117 | }), 118 | options 119 | ); 120 | }; 121 | }); 122 | 123 | fill(proto, "removeEventListener", function( 124 | original: () => void 125 | ): ( 126 | this: any, 127 | eventName: string, 128 | fn: EventListenerObject, 129 | options?: boolean | EventListenerOptions 130 | ) => () => void { 131 | return function( 132 | this: any, 133 | eventName: string, 134 | fn: EventListenerObject, 135 | options?: boolean | EventListenerOptions 136 | ): () => void { 137 | let callback = (fn as any) as WrappedFunction; 138 | try { 139 | callback = callback && (callback.__sentry_wrapped__ || callback); 140 | } catch (e) { 141 | // ignore, accessing __sentry_wrapped__ will throw in some Selenium environments 142 | } 143 | return original.call(this, eventName, callback, options); 144 | }; 145 | }); 146 | } 147 | 148 | /** 149 | * Wrap timer functions and event targets to catch errors 150 | * and provide better metadata. 151 | */ 152 | public setupOnce(): void { 153 | this._ignoreOnError = this._ignoreOnError; 154 | 155 | const global = getGlobalObject(); 156 | 157 | fill(global, "setTimeout", this._wrapTimeFunction.bind(this)); 158 | fill(global, "setInterval", this._wrapTimeFunction.bind(this)); 159 | fill(global, "requestAnimationFrame", this._wrapRAF.bind(this)); 160 | 161 | [ 162 | "EventTarget", 163 | "Window", 164 | "Node", 165 | "ApplicationCache", 166 | "AudioTrackList", 167 | "ChannelMergerNode", 168 | "CryptoOperation", 169 | "EventSource", 170 | "FileReader", 171 | "HTMLUnknownElement", 172 | "IDBDatabase", 173 | "IDBRequest", 174 | "IDBTransaction", 175 | "KeyOperation", 176 | "MediaController", 177 | "MessagePort", 178 | "ModalWindow", 179 | "Notification", 180 | "SVGElementInstance", 181 | "Screen", 182 | "TextTrack", 183 | "TextTrackCue", 184 | "TextTrackList", 185 | "WebSocket", 186 | "WebSocketWorker", 187 | "Worker", 188 | "XMLHttpRequest", 189 | "XMLHttpRequestEventTarget", 190 | "XMLHttpRequestUpload" 191 | ].forEach(this._wrapEventTarget.bind(this)); 192 | } 193 | } 194 | 195 | /** 196 | * Safely extract function name from itself 197 | */ 198 | function getFunctionName(fn: any): string { 199 | try { 200 | return (fn && fn.name) || ""; 201 | } catch (e) { 202 | // Just accessing custom props in some Selenium environments 203 | // can cause a "Permission denied" exception (see raven-js#495). 204 | return ""; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/integrations/globalhandlers.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentHub } from "@sentry/core"; 2 | import { Integration } from "@sentry/types"; 3 | import { logger } from "@sentry/utils"; 4 | 5 | import { sdk } from "../crossPlatform"; 6 | 7 | /** JSDoc */ 8 | interface GlobalHandlersIntegrations { 9 | onerror: boolean; 10 | onunhandledrejection: boolean; 11 | onpagenotfound: boolean; 12 | onmemorywarning: boolean; 13 | } 14 | 15 | /** Global handlers */ 16 | export class GlobalHandlers implements Integration { 17 | /** 18 | * @inheritDoc 19 | */ 20 | public name: string = GlobalHandlers.id; 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public static id: string = "GlobalHandlers"; 26 | 27 | /** JSDoc */ 28 | private readonly _options: GlobalHandlersIntegrations; 29 | 30 | /** JSDoc */ 31 | private _onErrorHandlerInstalled: boolean = false; 32 | 33 | /** JSDoc */ 34 | private _onUnhandledRejectionHandlerInstalled: boolean = false; 35 | 36 | /** JSDoc */ 37 | private _onPageNotFoundHandlerInstalled: boolean = false; 38 | 39 | /** JSDoc */ 40 | private _onMemoryWarningHandlerInstalled: boolean = false; 41 | 42 | /** JSDoc */ 43 | public constructor(options?: GlobalHandlersIntegrations) { 44 | this._options = { 45 | onerror: true, 46 | onunhandledrejection: true, 47 | onpagenotfound: true, 48 | onmemorywarning: true, 49 | ...options, 50 | }; 51 | } 52 | 53 | /** JSDoc */ 54 | public setExtraOptions(extraOptions?: any): void { 55 | if (extraOptions) { 56 | if (extraOptions.onerror !== undefined) { 57 | this._options.onerror = !!extraOptions.onerror; 58 | } 59 | 60 | if (extraOptions.onunhandledrejection !== undefined) { 61 | this._options.onunhandledrejection = !!extraOptions.onunhandledrejection; 62 | } 63 | 64 | if (extraOptions.onpagenotfound !== undefined) { 65 | this._options.onpagenotfound = !!extraOptions.onpagenotfound; 66 | } 67 | 68 | if (extraOptions.onmemorywarning !== undefined) { 69 | this._options.onmemorywarning = !!extraOptions.onmemorywarning; 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | public setupOnce(): void { 78 | Error.stackTraceLimit = 50; 79 | 80 | if (this._options.onerror) { 81 | logger.log("Global Handler attached: onError"); 82 | this._installGlobalOnErrorHandler(); 83 | } 84 | 85 | if (this._options.onunhandledrejection) { 86 | logger.log("Global Handler attached: onunhandledrejection"); 87 | this._installGlobalOnUnhandledRejectionHandler(); 88 | } 89 | 90 | if (this._options.onpagenotfound) { 91 | logger.log("Global Handler attached: onPageNotFound"); 92 | this._installGlobalOnPageNotFoundHandler(); 93 | } 94 | 95 | if (this._options.onmemorywarning) { 96 | logger.log("Global Handler attached: onMemoryWarning"); 97 | this._installGlobalOnMemoryWarningHandler(); 98 | } 99 | } 100 | 101 | /** JSDoc */ 102 | private _installGlobalOnErrorHandler(): void { 103 | if (this._onErrorHandlerInstalled) { 104 | return; 105 | } 106 | 107 | if (!!sdk.onError) { 108 | const currentHub = getCurrentHub(); 109 | 110 | // https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onError.html 111 | sdk.onError((err: string | object) => { 112 | // console.info("sentry-uniapp", err); 113 | const error = typeof err === 'string' ? new Error(err) : err 114 | currentHub.captureException(error); 115 | }); 116 | } 117 | 118 | this._onErrorHandlerInstalled = true; 119 | } 120 | 121 | /** JSDoc */ 122 | private _installGlobalOnUnhandledRejectionHandler(): void { 123 | if (this._onUnhandledRejectionHandlerInstalled) { 124 | return; 125 | } 126 | 127 | if (!!sdk.onUnhandledRejection) { 128 | const currentHub = getCurrentHub(); 129 | /** JSDoc */ 130 | interface OnUnhandledRejectionRes { 131 | reason: string | object; 132 | promise: Promise; 133 | } 134 | 135 | // https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html 136 | sdk.onUnhandledRejection( 137 | ({ reason, promise }: OnUnhandledRejectionRes) => { 138 | // console.log(reason, typeof reason, promise) 139 | // 为什么官方文档上说 reason 是 string 类型,但是实际返回的确实 object 类型 140 | const error = typeof reason === 'string' ? new Error(reason) : reason 141 | currentHub.captureException(error, { 142 | data: promise, 143 | }); 144 | } 145 | ); 146 | } 147 | 148 | this._onUnhandledRejectionHandlerInstalled = true; 149 | } 150 | 151 | /** JSDoc */ 152 | private _installGlobalOnPageNotFoundHandler(): void { 153 | if (this._onPageNotFoundHandlerInstalled) { 154 | return; 155 | } 156 | 157 | if (!!sdk.onPageNotFound) { 158 | const currentHub = getCurrentHub(); 159 | 160 | sdk.onPageNotFound((res: { path: string }) => { 161 | const url = res.path.split("?")[0]; 162 | 163 | currentHub.setTag("pagenotfound", url); 164 | currentHub.setExtra("message", JSON.stringify(res)); 165 | currentHub.captureMessage(`页面无法找到: ${url}`); 166 | }); 167 | } 168 | 169 | this._onPageNotFoundHandlerInstalled = true; 170 | } 171 | 172 | /** JSDoc */ 173 | private _installGlobalOnMemoryWarningHandler(): void { 174 | if (this._onMemoryWarningHandlerInstalled) { 175 | return; 176 | } 177 | 178 | if (!!sdk.onMemoryWarning) { 179 | const currentHub = getCurrentHub(); 180 | 181 | sdk.onMemoryWarning(({ level = -1 }: { level: number }) => { 182 | let levelMessage = "没有获取到告警级别信息"; 183 | 184 | switch (level) { 185 | case 5: 186 | levelMessage = "TRIM_MEMORY_RUNNING_MODERATE"; 187 | break; 188 | case 10: 189 | levelMessage = "TRIM_MEMORY_RUNNING_LOW"; 190 | break; 191 | case 15: 192 | levelMessage = "TRIM_MEMORY_RUNNING_CRITICAL"; 193 | break; 194 | default: 195 | return; 196 | } 197 | 198 | currentHub.setTag("memory-warning", String(level)); 199 | currentHub.setExtra("message", levelMessage); 200 | currentHub.captureMessage(`内存不足告警`); 201 | }); 202 | } 203 | 204 | this._onMemoryWarningHandlerInstalled = true; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { captureException, getCurrentHub, withScope } from '@sentry/core'; 2 | import { Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types'; 3 | import { addExceptionMechanism, addExceptionTypeValue, htmlTreeAsString, normalize } from '@sentry/utils'; 4 | 5 | const debounceDuration: number = 1000; 6 | let keypressTimeout: number | undefined; 7 | let lastCapturedEvent: Event | undefined; 8 | let ignoreOnError: number = 0; 9 | 10 | /** 11 | * @hidden 12 | */ 13 | export function shouldIgnoreOnError(): boolean { 14 | return ignoreOnError > 0; 15 | } 16 | 17 | /** 18 | * @hidden 19 | */ 20 | export function ignoreNextOnError(): void { 21 | // onerror should trigger before setTimeout 22 | ignoreOnError += 1; 23 | setTimeout(() => { 24 | ignoreOnError -= 1; 25 | }); 26 | } 27 | 28 | /** 29 | * Instruments the given function and sends an event to Sentry every time the 30 | * function throws an exception. 31 | * 32 | * @param fn A function to wrap. 33 | * @returns The wrapped function. 34 | * @hidden 35 | */ 36 | export function wrap( 37 | fn: WrappedFunction, 38 | options: { 39 | mechanism?: Mechanism; 40 | capture?: boolean; 41 | } = {}, 42 | before?: WrappedFunction, 43 | ): any { 44 | // tslint:disable-next-line:strict-type-predicates 45 | if (typeof fn !== 'function') { 46 | return fn; 47 | } 48 | 49 | try { 50 | // We don't wanna wrap it twice 51 | if (fn.__sentry__) { 52 | return fn; 53 | } 54 | 55 | // If this has already been wrapped in the past, return that wrapped function 56 | if (fn.__sentry_wrapped__) { 57 | return fn.__sentry_wrapped__; 58 | } 59 | } catch (e) { 60 | // Just accessing custom props in some Selenium environments 61 | // can cause a "Permission denied" exception (see raven-js#495). 62 | // Bail on wrapping and return the function as-is (defers to window.onerror). 63 | return fn; 64 | } 65 | 66 | const sentryWrapped: WrappedFunction = function (this: any): void { 67 | // tslint:disable-next-line:strict-type-predicates 68 | if (before && typeof before === 'function') { 69 | before.apply(this, arguments); 70 | } 71 | 72 | const args = Array.prototype.slice.call(arguments); 73 | 74 | // tslint:disable:no-unsafe-any 75 | try { 76 | const wrappedArguments = args.map((arg: any) => wrap(arg, options)); 77 | 78 | if (fn.handleEvent) { 79 | // Attempt to invoke user-land function 80 | // NOTE: If you are a Sentry user, and you are seeing this stack frame, it 81 | // means the sentry.javascript SDK caught an error invoking your application code. This 82 | // is expected behavior and NOT indicative of a bug with sentry.javascript. 83 | return fn.handleEvent.apply(this, wrappedArguments); 84 | } 85 | 86 | // Attempt to invoke user-land function 87 | // NOTE: If you are a Sentry user, and you are seeing this stack frame, it 88 | // means the sentry.javascript SDK caught an error invoking your application code. This 89 | // is expected behavior and NOT indicative of a bug with sentry.javascript. 90 | return fn.apply(this, wrappedArguments); 91 | // tslint:enable:no-unsafe-any 92 | } catch (ex) { 93 | ignoreNextOnError(); 94 | 95 | withScope((scope: Scope) => { 96 | scope.addEventProcessor((event: SentryEvent) => { 97 | const processedEvent = { ...event }; 98 | 99 | if (options.mechanism) { 100 | addExceptionTypeValue(processedEvent, undefined, undefined); 101 | addExceptionMechanism(processedEvent, options.mechanism); 102 | } 103 | 104 | processedEvent.extra = { 105 | ...processedEvent.extra, 106 | arguments: normalize(args, 3), 107 | }; 108 | 109 | return processedEvent; 110 | }); 111 | 112 | captureException(ex); 113 | }); 114 | 115 | throw ex; 116 | } 117 | }; 118 | 119 | // Accessing some objects may throw 120 | // ref: https://github.com/getsentry/sentry-javascript/issues/1168 121 | try { 122 | // tslint:disable-next-line: no-for-in 123 | for (const property in fn) { 124 | if (Object.prototype.hasOwnProperty.call(fn, property)) { 125 | sentryWrapped[property] = fn[property]; 126 | } 127 | } 128 | } catch (_oO) { } // tslint:disable-line:no-empty 129 | 130 | fn.prototype = fn.prototype || {}; 131 | sentryWrapped.prototype = fn.prototype; 132 | 133 | Object.defineProperty(fn, '__sentry_wrapped__', { 134 | enumerable: false, 135 | value: sentryWrapped, 136 | }); 137 | 138 | // Signal that this function has been wrapped/filled already 139 | // for both debugging and to prevent it to being wrapped/filled twice 140 | Object.defineProperties(sentryWrapped, { 141 | __sentry__: { 142 | enumerable: false, 143 | value: true, 144 | }, 145 | __sentry_original__: { 146 | enumerable: false, 147 | value: fn, 148 | }, 149 | }); 150 | 151 | // Restore original function name (not all browsers allow that) 152 | try { 153 | const descriptor = Object.getOwnPropertyDescriptor(sentryWrapped, 'name') as PropertyDescriptor; 154 | if (descriptor.configurable) { 155 | Object.defineProperty(sentryWrapped, 'name', { 156 | get(): string { 157 | return fn.name; 158 | }, 159 | }); 160 | } 161 | } catch (_oO) { 162 | /*no-empty*/ 163 | } 164 | 165 | return sentryWrapped; 166 | } 167 | 168 | let debounceTimer: number = 0; 169 | 170 | /** 171 | * Wraps addEventListener to capture UI breadcrumbs 172 | * @param eventName the event name (e.g. "click") 173 | * @returns wrapped breadcrumb events handler 174 | * @hidden 175 | */ 176 | export function breadcrumbEventHandler(eventName: string, debounce: boolean = false): (event: Event) => void { 177 | return (event: Event) => { 178 | // reset keypress timeout; e.g. triggering a 'click' after 179 | // a 'keypress' will reset the keypress debounce so that a new 180 | // set of keypresses can be recorded 181 | keypressTimeout = undefined; 182 | // It's possible this handler might trigger multiple times for the same 183 | // event (e.g. event propagation through node ancestors). Ignore if we've 184 | // already captured the event. 185 | // tslint:disable-next-line: strict-comparisons 186 | if (!event || lastCapturedEvent === event) { 187 | return; 188 | } 189 | 190 | lastCapturedEvent = event; 191 | 192 | const captureBreadcrumb = () => { 193 | let target; 194 | 195 | // Accessing event.target can throw (see getsentry/raven-js#838, #768) 196 | try { 197 | target = event.target ? htmlTreeAsString(event.target as Node) : htmlTreeAsString((event as unknown) as Node); 198 | } catch (e) { 199 | target = ''; 200 | } 201 | 202 | if (target.length === 0) { 203 | return; 204 | } 205 | 206 | getCurrentHub().addBreadcrumb( 207 | { 208 | category: `ui.${eventName}`, // e.g. ui.click, ui.input 209 | message: target, 210 | }, 211 | { 212 | event, 213 | name: eventName, 214 | }, 215 | ); 216 | }; 217 | 218 | if (debounceTimer) { 219 | clearTimeout(debounceTimer); 220 | } 221 | 222 | if (debounce) { 223 | debounceTimer = setTimeout(captureBreadcrumb); 224 | } else { 225 | captureBreadcrumb(); 226 | } 227 | }; 228 | } 229 | 230 | /** 231 | * Wraps addEventListener to capture keypress UI events 232 | * @returns wrapped keypress events handler 233 | * @hidden 234 | */ 235 | export function keypressEventHandler(): (event: Event) => void { 236 | // TODO: if somehow user switches keypress target before 237 | // debounce timeout is triggered, we will only capture 238 | // a single breadcrumb from the FIRST target (acceptable?) 239 | return (event: Event) => { 240 | let target; 241 | 242 | try { 243 | target = event.target; 244 | } catch (e) { 245 | // just accessing event properties can throw an exception in some rare circumstances 246 | // see: https://github.com/getsentry/raven-js/issues/838 247 | return; 248 | } 249 | 250 | const tagName = target && (target as HTMLElement).tagName; 251 | 252 | // only consider keypress events on actual input elements 253 | // this will disregard keypresses targeting body (e.g. tabbing 254 | // through elements, hotkeys, etc) 255 | if (!tagName || (tagName !== 'INPUT' && tagName !== 'TEXTAREA' && !(target as HTMLElement).isContentEditable)) { 256 | return; 257 | } 258 | 259 | // record first keypress in a series, but ignore subsequent 260 | // keypresses until debounce clears 261 | if (!keypressTimeout) { 262 | breadcrumbEventHandler('input')(event); 263 | } 264 | clearTimeout(keypressTimeout); 265 | 266 | keypressTimeout = (setTimeout(() => { 267 | keypressTimeout = undefined; 268 | }, debounceDuration) as any) as number; 269 | }; 270 | } 271 | -------------------------------------------------------------------------------- /src/tracekit.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-sort-keys 2 | 3 | /** 4 | * This was originally forked from https://github.com/occ/TraceKit, but has since been 5 | * largely modified and is now maintained as part of Sentry JS SDK. 6 | */ 7 | 8 | /** 9 | * An object representing a single stack frame. 10 | * {Object} StackFrame 11 | * {string} url The JavaScript or HTML file URL. 12 | * {string} func The function name, or empty for anonymous functions (if guessing did not work). 13 | * {string[]?} args The arguments passed to the function, if known. 14 | * {number=} line The line number, if known. 15 | * {number=} column The column number, if known. 16 | * {string[]} context An array of source code lines; the middle element corresponds to the correct line#. 17 | */ 18 | export interface StackFrame { 19 | url: string; 20 | func: string; 21 | args: string[]; 22 | line: number | null; 23 | column: number | null; 24 | } 25 | 26 | /** 27 | * An object representing a JavaScript stack trace. 28 | * {Object} StackTrace 29 | * {string} name The name of the thrown exception. 30 | * {string} message The exception error message. 31 | * {TraceKit.StackFrame[]} stack An array of stack frames. 32 | */ 33 | export interface StackTrace { 34 | name: string; 35 | message: string; 36 | mechanism?: string; 37 | stack: StackFrame[]; 38 | failed?: boolean; 39 | } 40 | 41 | // global reference to slice 42 | const UNKNOWN_FUNCTION = "?"; 43 | 44 | // Chromium based browsers: Chrome, Brave, new Opera, new Edge 45 | const chrome = /^\s*at (?:(.*?) ?\()?((?:file|https?|blob|chrome-extension|native|eval|webpack||[-a-z]+:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; 46 | // gecko regex: `(?:bundle|\d+\.js)`: `bundle` is for react native, `\d+\.js` also but specifically for ram bundles because it 47 | // generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js 48 | // We need this specific case for now because we want no other regex to match. 49 | const gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension).*?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js))(?::(\d+))?(?::(\d+))?\s*$/i; 50 | const winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; 51 | const geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; 52 | const chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/; 53 | const miniapp = /^\s*at (\w.*) \((\w*.js):(\d*):(\d*)/i; 54 | 55 | /** JSDoc */ 56 | export function computeStackTrace(ex: any): StackTrace { 57 | // console.log('computeStackTrace', ex) 58 | // tslint:disable:no-unsafe-any 59 | 60 | let stack = null; 61 | const popSize: number = ex && ex.framesToPop; 62 | 63 | try { 64 | // This must be tried first because Opera 10 *destroys* 65 | // its stacktrace property if you try to access the stack 66 | // property first!! 67 | stack = computeStackTraceFromStacktraceProp(ex); 68 | if (stack) { 69 | // console.log('computeStackTraceFromStacktraceProp', stack, popSize, popFrames(stack, popSize)) 70 | return popFrames(stack, popSize); 71 | } 72 | } catch (e) { 73 | // no-empty 74 | } 75 | 76 | try { 77 | stack = computeStackTraceFromStackProp(ex); 78 | if (stack) { 79 | // console.log('computeStackTraceFromStackProp', stack, popSize, popFrames(stack, popSize)) 80 | return popFrames(stack, popSize); 81 | } 82 | } catch (e) { 83 | // no-empty 84 | } 85 | 86 | return { 87 | message: extractMessage(ex), 88 | name: ex && ex.name, 89 | stack: [], 90 | failed: true, 91 | }; 92 | } 93 | 94 | /** JSDoc */ 95 | // tslint:disable-next-line:cyclomatic-complexity 96 | function computeStackTraceFromStackProp(ex: any): StackTrace | null { 97 | // tslint:disable:no-conditional-assignment 98 | if (!ex || !ex.stack) { 99 | return null; 100 | } 101 | 102 | const stack = []; 103 | const lines = ex.stack.split("\n"); 104 | let isEval; 105 | let submatch; 106 | let parts; 107 | let element; 108 | // console.log('lines', lines) 109 | 110 | for (let i = 0; i < lines.length; ++i) { 111 | // console.log(lines[i], chrome.exec(lines[i]), winjs.exec(lines[i]), gecko.exec(lines[i])) 112 | if ((parts = chrome.exec(lines[i]))) { 113 | const isNative = parts[2] && parts[2].indexOf("native") === 0; // start of line 114 | isEval = parts[2] && parts[2].indexOf("eval") === 0; // start of line 115 | if (isEval && (submatch = chromeEval.exec(parts[2]))) { 116 | // throw out eval line/column and use top-most line/column number 117 | parts[2] = submatch[1]; // url 118 | parts[3] = submatch[2]; // line 119 | parts[4] = submatch[3]; // column 120 | } 121 | element = { 122 | url: parts[2], 123 | func: parts[1] || UNKNOWN_FUNCTION, 124 | args: isNative ? [parts[2]] : [], 125 | line: parts[3] ? +parts[3] : null, 126 | column: parts[4] ? +parts[4] : null, 127 | }; 128 | } else if ((parts = winjs.exec(lines[i]))) { 129 | element = { 130 | url: parts[2], 131 | func: parts[1] || UNKNOWN_FUNCTION, 132 | args: [], 133 | line: +parts[3], 134 | column: parts[4] ? +parts[4] : null, 135 | }; 136 | } else if ((parts = gecko.exec(lines[i]))) { 137 | isEval = parts[3] && parts[3].indexOf(" > eval") > -1; 138 | if (isEval && (submatch = geckoEval.exec(parts[3]))) { 139 | // throw out eval line/column and use top-most line number 140 | parts[1] = parts[1] || `eval`; 141 | parts[3] = submatch[1]; 142 | parts[4] = submatch[2]; 143 | parts[5] = ""; // no column when eval 144 | } else if (i === 0 && !parts[5] && ex.columnNumber !== void 0) { 145 | // FireFox uses this awesome columnNumber property for its top frame 146 | // Also note, Firefox's column number is 0-based and everything else expects 1-based, 147 | // so adding 1 148 | // NOTE: this hack doesn't work if top-most frame is eval 149 | stack[0].column = (ex.columnNumber as number) + 1; 150 | } 151 | element = { 152 | url: parts[3], 153 | func: parts[1] || UNKNOWN_FUNCTION, 154 | args: parts[2] ? parts[2].split(",") : [], 155 | line: parts[4] ? +parts[4] : null, 156 | column: parts[5] ? +parts[5] : null, 157 | }; 158 | } else if ((parts = miniapp.exec(lines[i]))) { 159 | element = { 160 | url: parts[2], 161 | func: parts[1] || UNKNOWN_FUNCTION, 162 | args: [], 163 | line: parts[3] ? +parts[3] : null, 164 | column: parts[4] ? +parts[4] : null, 165 | }; 166 | } else { 167 | continue; 168 | } 169 | 170 | if (!element.func && element.line) { 171 | element.func = UNKNOWN_FUNCTION; 172 | } 173 | 174 | stack.push(element); 175 | } 176 | 177 | if (!stack.length) { 178 | return null; 179 | } 180 | 181 | return { 182 | message: extractMessage(ex), 183 | name: ex.name, 184 | stack, 185 | }; 186 | } 187 | 188 | /** JSDoc */ 189 | function computeStackTraceFromStacktraceProp(ex: any): StackTrace | null { 190 | if (!ex || !ex.stacktrace) { 191 | return null; 192 | } 193 | // Access and store the stacktrace property before doing ANYTHING 194 | // else to it because Opera is not very good at providing it 195 | // reliably in other circumstances. 196 | const stacktrace = ex.stacktrace; 197 | const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i; 198 | const opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i; 199 | const lines = stacktrace.split("\n"); 200 | const stack = []; 201 | let parts; 202 | 203 | for (let line = 0; line < lines.length; line += 2) { 204 | // tslint:disable:no-conditional-assignment 205 | let element = null; 206 | if ((parts = opera10Regex.exec(lines[line]))) { 207 | element = { 208 | url: parts[2], 209 | func: parts[3], 210 | args: [], 211 | line: +parts[1], 212 | column: null, 213 | }; 214 | } else if ((parts = opera11Regex.exec(lines[line]))) { 215 | element = { 216 | url: parts[6], 217 | func: parts[3] || parts[4], 218 | args: parts[5] ? parts[5].split(",") : [], 219 | line: +parts[1], 220 | column: +parts[2], 221 | }; 222 | } 223 | 224 | if (element) { 225 | if (!element.func && element.line) { 226 | element.func = UNKNOWN_FUNCTION; 227 | } 228 | stack.push(element); 229 | } 230 | } 231 | 232 | if (!stack.length) { 233 | return null; 234 | } 235 | 236 | return { 237 | message: extractMessage(ex), 238 | name: ex.name, 239 | stack, 240 | }; 241 | } 242 | 243 | /** Remove N number of frames from the stack */ 244 | function popFrames(stacktrace: StackTrace, popSize: number): StackTrace { 245 | try { 246 | return { 247 | ...stacktrace, 248 | stack: stacktrace.stack.slice(popSize), 249 | }; 250 | } catch (e) { 251 | return stacktrace; 252 | } 253 | } 254 | 255 | /** 256 | * There are cases where stacktrace.message is an Event object 257 | * https://github.com/getsentry/sentry-javascript/issues/1949 258 | * In this specific case we try to extract stacktrace.message.error.message 259 | */ 260 | function extractMessage(ex: any): string { 261 | const message = ex && ex.message; 262 | // console.log('message',message) 263 | if (!message) { 264 | return "No error message"; 265 | } 266 | if (message.error && typeof message.error.message === "string") { 267 | return message.error.message; 268 | } 269 | return message; 270 | } 271 | --------------------------------------------------------------------------------