├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── example ├── browser │ ├── index.html │ ├── index.js │ ├── math.js │ ├── package.json │ └── text-area-logger.js ├── express │ ├── app.js │ └── package.json ├── react │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── HooksExampleApp.js │ │ ├── index.css │ │ ├── index.jsx │ │ └── setupTests.js │ └── vite.config.js ├── svelte-kit │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── jsconfig.json │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── hooks.client.js │ │ ├── hooks.server.js │ │ └── routes │ │ │ └── +page.svelte │ ├── static │ │ └── favicon.png │ ├── svelte.config.js │ └── vite.config.js └── vue │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ ├── components │ │ └── HelloWorld.vue │ └── main.js │ └── vite.config.js ├── package-lock.json ├── package.json ├── packages ├── angularjs │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── browser │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── BrowserExceptionlessClient.ts │ │ ├── index.ts │ │ └── plugins │ │ │ ├── BrowserErrorPlugin.ts │ │ │ ├── BrowserGlobalHandlerPlugin.ts │ │ │ ├── BrowserIgnoreExtensionErrorsPlugin.ts │ │ │ ├── BrowserLifeCyclePlugin.ts │ │ │ ├── BrowserModuleInfoPlugin.ts │ │ │ └── BrowserRequestInfoPlugin.ts │ ├── test │ │ └── plugins │ │ │ ├── BrowserErrorPlugin.test.ts │ │ │ └── BrowserIgnoreExtensionErrorsPlugin.test.ts │ └── tsconfig.json ├── core │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── EventBuilder.ts │ │ ├── ExceptionlessClient.ts │ │ ├── Utils.ts │ │ ├── configuration │ │ │ ├── Configuration.ts │ │ │ └── SettingsManager.ts │ │ ├── index.ts │ │ ├── lastReferenceIdManager │ │ │ ├── DefaultLastReferenceIdManager.ts │ │ │ └── ILastReferenceIdManager.ts │ │ ├── logging │ │ │ ├── ConsoleLog.ts │ │ │ ├── ILog.ts │ │ │ └── NullLog.ts │ │ ├── models │ │ │ ├── Event.ts │ │ │ ├── EventContext.ts │ │ │ └── data │ │ │ │ ├── EnvironmentInfo.ts │ │ │ │ ├── ErrorInfo.ts │ │ │ │ ├── ManualStackingInfo.ts │ │ │ │ ├── ModuleInfo.ts │ │ │ │ ├── RequestInfo.ts │ │ │ │ ├── UserDescription.ts │ │ │ │ └── UserInfo.ts │ │ ├── plugins │ │ │ ├── EventPluginContext.ts │ │ │ ├── EventPluginManager.ts │ │ │ ├── IEventPlugin.ts │ │ │ ├── PluginContext.ts │ │ │ └── default │ │ │ │ ├── ConfigurationDefaultsPlugin.ts │ │ │ │ ├── DuplicateCheckerPlugin.ts │ │ │ │ ├── EventExclusionPlugin.ts │ │ │ │ ├── HeartbeatPlugin.ts │ │ │ │ ├── ReferenceIdPlugin.ts │ │ │ │ ├── SessionIdManagementPlugin.ts │ │ │ │ ├── SimpleErrorPlugin.ts │ │ │ │ └── SubmissionMethodPlugin.ts │ │ ├── queue │ │ │ ├── DefaultEventQueue.ts │ │ │ └── IEventQueue.ts │ │ ├── storage │ │ │ ├── IStorage.ts │ │ │ ├── InMemoryStorage.ts │ │ │ └── LocalStorage.ts │ │ └── submission │ │ │ ├── DefaultSubmissionClient.ts │ │ │ ├── ISubmissionClient.ts │ │ │ └── Response.ts │ ├── test │ │ ├── ExceptionlessClient.test.ts │ │ ├── Utils.test.ts │ │ ├── configuration │ │ │ └── Configuration.test.ts │ │ ├── helpers.ts │ │ ├── plugins │ │ │ ├── EventPluginManager.test.ts │ │ │ └── default │ │ │ │ ├── ConfigurationDefaultsPlugin.test.ts │ │ │ │ ├── DuplicateCheckerPlugin.test.ts │ │ │ │ ├── EventExclusionPlugin.test.ts │ │ │ │ └── exceptions.ts │ │ ├── queue │ │ │ └── DefaultEventQueue.test.ts │ │ ├── storage │ │ │ ├── InMemoryStorage.test.ts │ │ │ ├── LocalStorage.test.ts │ │ │ └── StorageTestBase.ts │ │ └── submission │ │ │ ├── TestSubmissionClient.test.ts │ │ │ └── TestSubmissionClient.ts │ └── tsconfig.json ├── node │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── NodeExceptionlessClient.ts │ │ ├── index.ts │ │ ├── plugins │ │ │ ├── NodeEnvironmentInfoPlugin.ts │ │ │ ├── NodeErrorPlugin.ts │ │ │ ├── NodeGlobalHandlerPlugin.ts │ │ │ ├── NodeLifeCyclePlugin.ts │ │ │ ├── NodeRequestInfoPlugin.ts │ │ │ └── NodeWrapFunctions.ts │ │ └── storage │ │ │ └── NodeDirectoryStorage.ts │ ├── test │ │ └── storage │ │ │ └── NodeDirectoryStorage.test.ts │ └── tsconfig.json ├── react │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── ExceptionlessErrorBoundary.tsx │ │ └── index.ts │ └── tsconfig.json └── vue │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Exceptionless.JavaScript", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/typescript-node:latest", 4 | "extensions": [ 5 | "andys8.jest-snippets", 6 | "dbaeumer.vscode-eslint", 7 | "editorconfig.editorconfig", 8 | "esbenp.prettier-vscode", 9 | "firsttris.vscode-jest-runner", 10 | "hbenl.vscode-test-explorer", 11 | "juancasanova.awesometypescriptproblemmatcher", 12 | "ritwickdey.liveserver", 13 | "ryanluker.vscode-coverage-gutters", 14 | "streetsidesoftware.code-spell-checker" 15 | ], 16 | "forwardPorts": [3000], 17 | "postCreateCommand": "npm install" 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .eslintrc.js 3 | minver 4 | node_modules 5 | example/svelte-kit/.svelte-kit 6 | 7 | # Ignore files for PNPM, NPM and YARN 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:import/typescript", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 14 | "prettier" 15 | ], 16 | globals: { 17 | MutationObserver: "readonly", 18 | SharedArrayBuffer: "readonly", 19 | Atomics: "readonly", 20 | BigInt: "readonly", 21 | BigInt64Array: "readonly", 22 | BigUint64Array: "readonly" 23 | }, 24 | parser: "@typescript-eslint/parser", 25 | parserOptions: { 26 | ecmaVersion: 2022, 27 | sourceType: "module", 28 | project: ["./tsconfig.json"], 29 | tsconfigRootDir: __dirname 30 | }, 31 | plugins: ["@typescript-eslint", "import", "jest"], 32 | ignorePatterns: ["dist", "node_modules", "example"], 33 | rules: { 34 | "@typescript-eslint/no-inferrable-types": "off", 35 | "@typescript-eslint/no-unsafe-assignment": "off", 36 | "@typescript-eslint/no-unsafe-call": "off", 37 | "@typescript-eslint/no-unsafe-member-access": "off", 38 | "@typescript-eslint/no-unsafe-return": "off", 39 | "@typescript-eslint/no-redundant-type-constituents": "off", 40 | "@typescript-eslint/restrict-plus-operands": "off" 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | *.sh text eol=lf 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: exceptionless 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: 10 | - ubuntu-latest 11 | - macos-latest 12 | - windows-latest 13 | node_version: 14 | - 20 15 | name: Node ${{ matrix.node_version }} on ${{ matrix.os }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Build Reason 22 | run: "echo ref: ${{github.ref}} event: ${{github.event_name}}" 23 | - name: Setup Node.js environment 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node_version }} 27 | registry-url: "https://registry.npmjs.org" 28 | - name: Cache node_modules 29 | uses: actions/cache@v4 30 | with: 31 | path: node_modules 32 | key: ${{ matrix.node_version }}-${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }} 33 | - name: Set Min Version 34 | uses: Stelzi79/action-minver@3.0.1 35 | id: version 36 | with: 37 | minimum-major-minor: 3.0 38 | tag-prefix: v 39 | - name: Build Version 40 | run: | 41 | npm install --global replace-in-files-cli 42 | replace-in-files --string="3.0.0-dev" --replacement=${{steps.version.outputs.version}} packages/core/src/configuration/Configuration.ts 43 | replace-in-files --string="3.0.0-dev" --replacement=${{steps.version.outputs.version}} **/package*.json 44 | npm install 45 | - name: Build 46 | run: npm run build 47 | - name: Lint 48 | run: npm run lint 49 | - name: Run Tests 50 | run: npm test 51 | - name: Publish Release Packages 52 | if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-latest' 53 | run: npm publish --workspaces --access public 54 | env: 55 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 56 | - name: Setup GitHub CI Node.js environment 57 | if: github.event_name != 'pull_request' && matrix.os == 'ubuntu-latest' 58 | uses: actions/setup-node@v4 59 | with: 60 | node-version: ${{ matrix.node_version }} 61 | registry-url: "https://npm.pkg.github.com" 62 | scope: "@exceptionless" 63 | - name: Push GitHub CI Packages 64 | if: github.event_name != 'pull_request' && matrix.os == 'ubuntu-latest' 65 | run: npm publish --workspaces --access public 66 | env: 67 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .git 4 | node_modules 5 | bower_components 6 | dist 7 | *.eslintcache 8 | *.tsbuildinfo 9 | 10 | test-data 11 | packages/node/test-data 12 | 13 | yarn.lock 14 | .exceptionless 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | minver 4 | example/svelte-kit/.svelte-kit 5 | 6 | # Ignore files for PNPM, NPM and YARN 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "andys8.jest-snippets", 4 | "davidanson.vscode-markdownlint", 5 | "dbaeumer.vscode-eslint", 6 | "editorconfig.editorconfig", 7 | "esbenp.prettier-vscode", 8 | "firsttris.vscode-jest-runner", 9 | "hbenl.vscode-test-explorer", 10 | "juancasanova.awesometypescriptproblemmatcher", 11 | "ritwickdey.liveserver", 12 | "ryanluker.vscode-coverage-gutters", 13 | "streetsidesoftware.code-spell-checker" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Express", 6 | "program": "${workspaceRoot}/example/express/app.js", 7 | "request": "launch", 8 | "preLaunchTask": "npm: build", 9 | "cwd": "${workspaceRoot}/example/express", 10 | "skipFiles": ["/**"], 11 | "type": "pwa-node" 12 | }, 13 | { 14 | "name": "Test", 15 | "request": "launch", 16 | "type": "node", 17 | "program": "${workspaceFolder}/node_modules/.bin/jest", 18 | "args": ["--runInBand"], 19 | "console": "integratedTerminal", 20 | "internalConsoleOptions": "neverOpen", 21 | "disableOptimisticBPs": true, 22 | "windows": { 23 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 24 | }, 25 | "cwd": "${workspaceRoot}" 26 | }, 27 | { 28 | "name": "Test Current File", 29 | "request": "launch", 30 | "type": "node", 31 | "program": "${workspaceFolder}/node_modules/.bin/jest", 32 | "args": ["${fileBasenameNoExtension}"], 33 | "console": "integratedTerminal", 34 | "internalConsoleOptions": "neverOpen", 35 | "disableOptimisticBPs": true, 36 | "windows": { 37 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 38 | }, 39 | "cwd": "${workspaceRoot}" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 5 | "files.insertFinalNewline": true, 6 | "files.exclude": { 7 | "**/tsconfig.tsbuildinfo": true, 8 | "**/node_modules": true, 9 | "test-data": true 10 | }, 11 | "search.exclude": { 12 | "**/node_modules": true 13 | }, 14 | "prettier.eslintIntegration": true, 15 | "javascript.preferences.quoteStyle": "double", 16 | "typescript.preferences.quoteStyle": "double", 17 | "typescript.tsdk": "./node_modules/typescript/lib", 18 | "cSpell.words": [ 19 | "Exceptionless", 20 | "angularjs", 21 | "bootstrapcdn", 22 | "configversion", 23 | "esbuild", 24 | "eslintignore", 25 | "jsdelivr", 26 | "localstorage", 27 | "maxcdn", 28 | "ncaught", 29 | "npmrc", 30 | "ratelimit", 31 | "sourceloc", 32 | "tsproject", 33 | "unfound", 34 | "vite", 35 | "vitejs", 36 | "webcompat" 37 | ], 38 | "eslint.validate": ["javascript", "typescript"], 39 | "deno.enable": false, 40 | "jest.jestCommandLine": "npm test --" 41 | } 42 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "clean", 9 | "problemMatcher": [], 10 | "label": "npm: clean" 11 | }, 12 | { 13 | "type": "npm", 14 | "script": "build", 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | }, 19 | "problemMatcher": "$tsc", 20 | "label": "npm: build" 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "test", 25 | "group": { 26 | "kind": "test", 27 | "isDefault": true 28 | }, 29 | "label": "npm: test" 30 | }, 31 | { 32 | "type": "npm", 33 | "script": "lint", 34 | "problemMatcher": "$eslint-stylish", 35 | "label": "npm: lint" 36 | }, 37 | { 38 | "type": "npm", 39 | "script": "watch --workspace=packages/browser", 40 | "isBackground": true, 41 | "problemMatcher": "$tsc", 42 | "label": "npm: watch browser" 43 | }, 44 | { 45 | "type": "npm", 46 | "script": "watch --workspace=packages/core", 47 | "isBackground": true, 48 | "problemMatcher": "$tsc", 49 | "label": "npm: watch core" 50 | }, 51 | { 52 | "type": "npm", 53 | "script": "watch --workspace=packages/react", 54 | "isBackground": true, 55 | "problemMatcher": "$tsc", 56 | "label": "npm: watch react" 57 | }, 58 | { 59 | "type": "npm", 60 | "script": "watch --workspace=packages/vue", 61 | "isBackground": true, 62 | "problemMatcher": "$tsc", 63 | "label": "npm: watch vue" 64 | }, 65 | { 66 | "type": "npm", 67 | "script": "watch --workspace=packages/node", 68 | "isBackground": true, 69 | "problemMatcher": "$tsc", 70 | "label": "npm: watch node" 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /example/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Exceptionless Test 6 | 7 | 8 |

Error Submission

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

Log Submission

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

Diagnostics

30 | 31 | 32 |

Benchmark

33 | 34 | 35 |

Client Logs

36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/browser/math.js: -------------------------------------------------------------------------------- 1 | export function divide(numerator, denominator) { 2 | if (denominator == 0) { 3 | throw new Error("Division by zero."); 4 | } 5 | 6 | return numerator / denominator; 7 | } 8 | -------------------------------------------------------------------------------- /example/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-sample", 3 | "private": true, 4 | "version": "3.0.0-dev", 5 | "description": "Exceptionless Sample Browser App", 6 | "main": "index.js", 7 | "type": "module", 8 | "author": "Exceptionless", 9 | "license": "Apache-2.0", 10 | "publishConfig": { 11 | "access": "restricted" 12 | }, 13 | "dependencies": { 14 | "@exceptionless/browser": "3.0.0-dev", 15 | "jquery": "^3.7.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/browser/text-area-logger.js: -------------------------------------------------------------------------------- 1 | export class TextAreaLogger { 2 | constructor(elementId, logger) { 3 | if (!elementId) { 4 | throw new Error("elementId is required"); 5 | } 6 | 7 | this.logger = logger; 8 | this.messageBuffer = []; 9 | if (document.readyState === "complete") { 10 | this.element = document.getElementById(elementId); 11 | } else { 12 | document.addEventListener("DOMContentLoaded", () => { 13 | this.element = document.getElementById(elementId); 14 | this.element.innerHTML = this.messageBuffer.join("\n") + this.element.innerHTML; 15 | this.messageBuffer = []; 16 | }); 17 | } 18 | } 19 | 20 | trace(message) { 21 | this.logger?.trace(message); 22 | this.log("debug", message); 23 | } 24 | info(message) { 25 | this.logger?.info(message); 26 | this.log("info", message); 27 | } 28 | warn(message) { 29 | this.logger?.warn(message); 30 | this.log("warn", message); 31 | } 32 | error(message) { 33 | this.logger?.error(message); 34 | this.log("error", message); 35 | } 36 | 37 | log(level, message) { 38 | const formattedMessage = `${new Date().toISOString()} [${level}] ${message}`; 39 | if (this.element) { 40 | this.element.innerHTML += `\n${formattedMessage}`; 41 | } else { 42 | this.messageBuffer.push(formattedMessage); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/express/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | const app = express(); 3 | 4 | import { Exceptionless, KnownEventDataKeys } from "@exceptionless/node"; 5 | 6 | await Exceptionless.startup((c) => { 7 | c.apiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; 8 | c.serverUrl = "https://localhost:5100"; 9 | c.useDebugLogger(); 10 | c.useLocalStorage(); 11 | c.usePersistedQueueStorage = true; 12 | 13 | c.defaultTags.push("Example", "Node"); 14 | 15 | // set some default data 16 | c.defaultData["SampleUser"] = { 17 | id: 1, 18 | name: "Blake", 19 | password: "123456", 20 | passwordResetToken: "a reset token", 21 | myPasswordValue: "123456", 22 | myPassword: "123456", 23 | customValue: "Password", 24 | value: { 25 | Password: "123456" 26 | } 27 | }; 28 | }); 29 | 30 | app.get("/", async (req, res) => { 31 | await Exceptionless.submitLog("loading index content"); 32 | res.send("Hello World!"); 33 | }); 34 | 35 | app.get("/about", async (req, res) => { 36 | await Exceptionless.submitFeatureUsage("about"); 37 | res.send("About"); 38 | }); 39 | 40 | app.get("/boom", function boom(req, res) { 41 | throw new Error("Boom!!"); 42 | }); 43 | 44 | app.get("/trycatch", async (req, res) => { 45 | try { 46 | throw new Error("Caught in try/catch"); 47 | } catch (error) { 48 | await Exceptionless.createException(error).setContextProperty(KnownEventDataKeys.RequestInfo, req).submit(); 49 | 50 | res.status(500).send("Error caught in try/catch"); 51 | } 52 | }); 53 | 54 | app.use(async (err, req, res, next) => { 55 | if (res.headersSent) { 56 | return next(err); 57 | } 58 | 59 | await Exceptionless.createUnhandledException(err, "express").setContextProperty(KnownEventDataKeys.RequestInfo, req).submit(); 60 | 61 | res.status(500).send("Something broke!"); 62 | }); 63 | 64 | app.use(async (req, res) => { 65 | await Exceptionless.createNotFound(req.originalUrl).setContextProperty(KnownEventDataKeys.RequestInfo, req).submit(); 66 | res.status(404).send("Sorry cant find that!"); 67 | }); 68 | 69 | const server = app.listen(3000, async () => { 70 | var host = server.address().address; 71 | var port = server.address().port; 72 | 73 | var message = "Example app listening at http://" + host + port; 74 | await Exceptionless.submitLog("app", message, "Info"); 75 | }); 76 | 77 | export default app; 78 | -------------------------------------------------------------------------------- /example/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-sample", 3 | "private": true, 4 | "version": "3.0.0-dev", 5 | "description": "Exceptionless Sample Node App", 6 | "main": "app.js", 7 | "type": "module", 8 | "author": "Exceptionless", 9 | "license": "Apache-2.0", 10 | "scripts": { 11 | "dev": "node app.js --enable-source-maps --watch", 12 | "start": "node app.js --enable-source-maps" 13 | }, 14 | "publishConfig": { 15 | "access": "restricted" 16 | }, 17 | "dependencies": { 18 | "express": "^4.19.2", 19 | "@exceptionless/node": "3.0.0-dev" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/react/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /example/react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example/react/README.md: -------------------------------------------------------------------------------- 1 | ## Exceptionless React Example 2 | 3 | This example shows how to use the `@exceptionless/react` package. There is both a class component example (App.js) and a function component example with hooks (HooksExampleApp.js). 4 | 5 | The package includes [error boundary support](https://reactjs.org/docs/error-boundaries.html) which means uncaught errors inside your components will automatically be sent to Exceptionless. 6 | 7 | To run locally, follow these steps: 8 | 9 | 1. `git clone https://github.com/exceptionless/Exceptionless.JavaScript` 10 | 2. `cd Exceptionless.Javascript` 11 | 3. `npm install` 12 | 4. `cd example/react` 13 | 5. `npm start` 14 | 15 | Reference the main `@exceptionless/react` [README](../../packages/react/README.md) here when building your own React app. 16 | -------------------------------------------------------------------------------- /example/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | React App 16 | 17 | 18 | 19 |
20 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-example", 3 | "private": true, 4 | "version": "3.0.0-dev", 5 | "scripts": { 6 | "start": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "eslintConfig": { 11 | "extends": [ 12 | "react-app", 13 | "react-app/jest" 14 | ] 15 | }, 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | }, 28 | "devDependencies": { 29 | "@testing-library/jest-dom": "^6.4.2", 30 | "@testing-library/react": "^14.2.2", 31 | "@testing-library/user-event": "^14.5.2", 32 | "@vitejs/plugin-react": "^4.2.1", 33 | "vite": "^5.2.6" 34 | }, 35 | "dependencies": { 36 | "@exceptionless/react": "3.0.0-dev", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0" 39 | }, 40 | "type": "module", 41 | "publishConfig": { 42 | "access": "restricted" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exceptionless/Exceptionless.JavaScript/6898ae6d1864043685b7c08c6586f7f07f4810bc/example/react/public/favicon.ico -------------------------------------------------------------------------------- /example/react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exceptionless/Exceptionless.JavaScript/6898ae6d1864043685b7c08c6586f7f07f4810bc/example/react/public/logo192.png -------------------------------------------------------------------------------- /example/react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exceptionless/Exceptionless.JavaScript/6898ae6d1864043685b7c08c6586f7f07f4810bc/example/react/public/logo512.png -------------------------------------------------------------------------------- /example/react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | .container { 32 | max-width: 85%; 33 | margin: auto; 34 | } 35 | 36 | @keyframes App-logo-spin { 37 | from { 38 | transform: rotate(0deg); 39 | } 40 | to { 41 | transform: rotate(360deg); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/react/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./App.css"; 3 | import { Exceptionless, ExceptionlessErrorBoundary } from "@exceptionless/react"; 4 | 5 | class App extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | error: false, 10 | message: "", 11 | errorInfo: "" 12 | }; 13 | } 14 | async componentDidMount() { 15 | await Exceptionless.startup((c) => { 16 | c.apiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; 17 | c.serverUrl = "https://localhost:5100"; 18 | c.useDebugLogger(); 19 | 20 | c.defaultTags.push("Example", "React"); 21 | }); 22 | } 23 | 24 | throwErrorInComponent = () => { 25 | this.setState({ error: true }); 26 | }; 27 | 28 | submitMessage = async () => { 29 | const message = "Hello, world!"; 30 | this.setState({ message: "", errorInfo: "" }); 31 | await Exceptionless.submitLog(message); 32 | this.setState({ message }); 33 | }; 34 | 35 | tryCatchExample = async () => { 36 | try { 37 | this.setState({ message: "", errorInfo: "" }); 38 | throw new Error("Caught in the try/catch"); 39 | } catch (error) { 40 | this.setState({ errorInfo: error.message }); 41 | await Exceptionless.submitException(error); 42 | } 43 | }; 44 | 45 | unhandledExceptionExample = () => { 46 | throw new Error("Unhandled exception"); 47 | }; 48 | 49 | renderExample = () => { 50 | if (this.state.error) { 51 | throw new Error("I crashed!"); 52 | } else { 53 | return ( 54 |
55 |
56 |
57 |

Exceptionless React Sample

58 |

By pressing the button below, an uncaught error will be thrown inside your component. This will automatically be sent to Exceptionless.

59 | 60 |
61 |

Throw an uncaught error and make sure Exceptionless tracks it.

62 | 63 |
64 |

The following buttons simulated handled events outside the component.

65 | 66 | {this.state.message && ( 67 |

68 | Message sent to Exceptionless: {this.state.message} 69 |

70 | )} 71 | 72 | {this.state.errorInfo && ( 73 |

74 | Error message sent to Exceptionless: {this.state.errorInfo} 75 |

76 | )} 77 |
78 |
79 |
80 | ); 81 | } 82 | }; 83 | 84 | render() { 85 | return {this.renderExample()}; 86 | } 87 | } 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /example/react/src/HooksExampleApp.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import "./App.css"; 3 | import { Exceptionless, ExceptionlessErrorBoundary } from "@exceptionless/react"; 4 | 5 | const HooksExampleApp = () => { 6 | const [error, setError] = useState(false); 7 | useEffect(() => { 8 | startExceptionless(); 9 | }, []); 10 | 11 | const startExceptionless = async () => { 12 | await Exceptionless.startup((c) => { 13 | c.apiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; 14 | c.serverUrl = "https://localhost:5100"; 15 | c.useDebugLogger(); 16 | 17 | c.defaultTags.push("Example", "React"); 18 | }); 19 | }; 20 | 21 | const throwErrorInComponent = () => { 22 | setError(true); 23 | }; 24 | 25 | const submitMessage = () => { 26 | Exceptionless.submitLog("Hello, world!"); 27 | }; 28 | 29 | const tryCatchExample = () => { 30 | try { 31 | throw new Error("Caught in the try/catch"); 32 | } catch (error) { 33 | Exceptionless.submitException(error); 34 | } 35 | }; 36 | 37 | const renderExample = () => { 38 | if (error) { 39 | throw new Error("I crashed!"); 40 | } else { 41 | return ( 42 |
43 |
44 |
45 |

Exceptionless React Sample

46 |

By pressing the button below, an uncaught error will be thrown inside your component. This will automatically be sent to Exceptionless.

47 | 48 |

The following buttons simulated handled events outside the component.

49 | 50 | 51 |
52 |
53 |
54 | ); 55 | } 56 | }; 57 | 58 | return {renderExample()}; 59 | }; 60 | 61 | export default HooksExampleApp; 62 | -------------------------------------------------------------------------------- /example/react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | } 7 | 8 | code { 9 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 10 | } 11 | -------------------------------------------------------------------------------- /example/react/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const container = document.getElementById("root"); 7 | const root = createRoot(container); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /example/react/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /example/react/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | base: "", 6 | plugins: [react()], 7 | server: { 8 | open: true, 9 | port: 5174 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /example/svelte-kit/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /example/svelte-kit/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /example/svelte-kit/README.md: -------------------------------------------------------------------------------- 1 | ## Exceptionless Svelte Kit Example 2 | 3 | This example shows how to use the `@exceptionless/browser` package for client side Svelte Kit and `@exceptionless/node` for server side Svelte Kit. These is both 4 | a client side error hook `hooks.client.js` and a server side error hook `hooks.server.js`. 5 | 6 | To run locally, follow these steps: 7 | 8 | 1. `git clone https://github.com/exceptionless/Exceptionless.JavaScript` 9 | 2. `cd Exceptionless.Javascript` 10 | 3. `npm install` 11 | 4. `cd example/svelte-kit` 12 | 5. `npm run dev -- --open` 13 | -------------------------------------------------------------------------------- /example/svelte-kit/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /example/svelte-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-kit", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" 11 | }, 12 | "dependencies": { 13 | "@exceptionless/browser": "3.0.0-dev" 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/adapter-auto": "^3.2.0", 17 | "@sveltejs/kit": "^2.5.5", 18 | "svelte": "^4.2.12", 19 | "svelte-check": "^3.6.8", 20 | "typescript": "^5.4.3", 21 | "vite": "^5.2.6" 22 | }, 23 | "type": "module", 24 | "publishConfig": { 25 | "access": "restricted" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/svelte-kit/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /example/svelte-kit/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/svelte-kit/src/hooks.client.js: -------------------------------------------------------------------------------- 1 | import { Exceptionless, toError } from "@exceptionless/browser"; 2 | 3 | Exceptionless.startup((c) => { 4 | c.apiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; 5 | c.serverUrl = "https://localhost:5100"; 6 | c.useDebugLogger(); 7 | 8 | c.defaultTags.push("Example", "svelte-kit", "client"); 9 | }); 10 | 11 | /** @type {import('@sveltejs/kit').HandleClientError} */ 12 | export async function handleError({ error, event }) { 13 | console.log("client error handler"); 14 | await Exceptionless.submitException(toError(error)); 15 | } 16 | -------------------------------------------------------------------------------- /example/svelte-kit/src/hooks.server.js: -------------------------------------------------------------------------------- 1 | import { Exceptionless, toError } from "@exceptionless/node"; 2 | 3 | Exceptionless.startup((c) => { 4 | c.apiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; 5 | c.serverUrl = "https://localhost:5100"; 6 | c.useDebugLogger(); 7 | 8 | c.defaultTags.push("Example", "svelte-kit", "server"); 9 | }); 10 | 11 | /** @type {import("@sveltejs/kit").HandleServerError} */ 12 | export async function handleError({ error, event }) { 13 | console.log("server error handler"); 14 | await Exceptionless.submitException(toError(error)); 15 | } 16 | -------------------------------------------------------------------------------- /example/svelte-kit/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 |
30 |
31 |

Exceptionless Svelte Sample

32 |
33 |

34 | Throw an uncaught error and make sure Exceptionless tracks it. 35 |

36 | 39 |
40 |

41 | The following buttons simulated handled events outside the 42 | component. 43 |

44 | 45 | {#if message} 46 |

47 | Message sent to Exceptionless:{" "} 48 | {message} 49 |

50 | {/if} 51 | 52 | {#if errorInfo} 53 |

54 | Error message sent to Exceptionless:{" "} 55 | {errorInfo} 56 |

57 | {/if} 58 |
59 |
60 |
61 | 62 | 83 | -------------------------------------------------------------------------------- /example/svelte-kit/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exceptionless/Exceptionless.JavaScript/6898ae6d1864043685b7c08c6586f7f07f4810bc/example/svelte-kit/static/favicon.png -------------------------------------------------------------------------------- /example/svelte-kit/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-auto"; 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | kit: { 6 | adapter: adapter() 7 | } 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /example/svelte-kit/vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /example/vue/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /example/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Exceptionless Vue Example 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-example", 3 | "private": true, 4 | "version": "3.0.0-dev", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.4.21", 12 | "@exceptionless/vue": "3.0.0-dev" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-vue": "^5.0.4", 16 | "@vue/compiler-sfc": "^3.4.21", 17 | "vite": "^5.2.6" 18 | }, 19 | "type": "module", 20 | "publishConfig": { 21 | "access": "restricted" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exceptionless/Exceptionless.JavaScript/6898ae6d1864043685b7c08c6586f7f07f4810bc/example/vue/public/favicon.ico -------------------------------------------------------------------------------- /example/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /example/vue/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /example/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import { Exceptionless, ExceptionlessErrorHandler } from "@exceptionless/vue"; 4 | 5 | Exceptionless.startup((c) => { 6 | c.useDebugLogger(); 7 | 8 | c.apiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; 9 | c.serverUrl = "https://localhost:5100"; 10 | c.updateSettingsWhenIdleInterval = 15000; 11 | c.usePersistedQueueStorage = true; 12 | c.setUserIdentity("12345678", "Blake"); 13 | 14 | // set some default data 15 | c.defaultData["SampleUser"] = { 16 | id: 1, 17 | name: "Blake", 18 | password: "123456", 19 | passwordResetToken: "a reset token", 20 | myPasswordValue: "123456", 21 | myPassword: "123456", 22 | customValue: "Password", 23 | value: { 24 | Password: "123456" 25 | } 26 | }; 27 | 28 | c.defaultTags.push("Example", "JavaScript", "Vue"); 29 | }); 30 | 31 | const app = createApp(App); 32 | app.config.errorHandler = ExceptionlessErrorHandler; 33 | app.mount("#app"); 34 | -------------------------------------------------------------------------------- /example/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exceptionless/monorepo", 3 | "private": true, 4 | "version": "3.0.0-dev", 5 | "license": "Apache-2.0", 6 | "jest": { 7 | "moduleFileExtensions": [ 8 | "js", 9 | "ts" 10 | ], 11 | "moduleNameMapper": { 12 | "^@exceptionless/(.*)$": "/packages/$1/src" 13 | }, 14 | "preset": "ts-jest", 15 | "resolver": "jest-ts-webcompat-resolver" 16 | }, 17 | "scripts": { 18 | "clean": "rimraf -g packages/*/dist example/*/dist", 19 | "build": "npm run build --workspaces --if-present", 20 | "watch": "npm run watch --workspaces --if-present", 21 | "lint": "run-s -c lint:eslint lint:prettier", 22 | "lint:eslint": "eslint .", 23 | "lint:prettier": "prettier --check .", 24 | "lint:fix": "eslint . --ext .ts --fix", 25 | "format": "prettier --write .", 26 | "test": "npm test --workspaces --if-present", 27 | "version": "npm --no-git-tag-version --workspaces=true version", 28 | "upgrade": "ncu -i", 29 | "upgrade:workspaces": "ncu -i --workspaces" 30 | }, 31 | "workspaces": [ 32 | "packages/core", 33 | "packages/browser", 34 | "packages/angularjs", 35 | "packages/node", 36 | "packages/react", 37 | "packages/vue", 38 | "example/*" 39 | ], 40 | "devDependencies": { 41 | "@typescript-eslint/eslint-plugin": "^6.16.0", 42 | "@typescript-eslint/parser": "^6.16.0", 43 | "eslint": "^8.56.0", 44 | "eslint-config-prettier": "^9.1.0", 45 | "eslint-plugin-eslint-comments": "^3.2.0", 46 | "eslint-plugin-eslint-plugin": "^5.2.1", 47 | "eslint-plugin-import": "^2.29.1", 48 | "eslint-plugin-jest": "^27.6.0", 49 | "eslint-plugin-jsdoc": "^46.9.1", 50 | "npm-run-all": "^4.1.5", 51 | "prettier": "^3.2.5", 52 | "rimraf": "^5.0.5", 53 | "typescript": "^5.3.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/angularjs/README.md: -------------------------------------------------------------------------------- 1 | # Exceptionless AngularJS 2 | 3 | This package provides native JavaScript support for AngularJS applications. 4 | 5 | ## Getting Started 6 | 7 | To use this package, your must be using ES6 and support ESM modules. 8 | 9 | ## Installation 10 | 11 | You can install Exceptionless either in your browser application using a `script` 12 | tag, or you can use the Node Package Manager (npm) to install the package. 13 | 14 | ### CDN 15 | 16 | Add the following script tag at the very beginning of your page: 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | ### npm 23 | 24 | 1. Install the package by running `npm install @exceptionless/angularjs --save`. 25 | 2. Add the following script tag at the very beginning of your page: 26 | 27 | ```html 28 | 29 | ``` 30 | 31 | ## Configuration 32 | 33 | 1. Import `exceptionless` angular module like this: `angular.module("app", ["exceptionless"])` 34 | 2. Inject `$ExceptionlessClient` and call startup during app startup. 35 | 36 | ```js 37 | angular 38 | .module("app", ["exceptionless"]) 39 | .config(function ($ExceptionlessClient) { 40 | await $ExceptionlessClient.startup((c) => { 41 | c.apiKey = "API_KEY_HERE"; 42 | 43 | c.defaultTags.push("Example", "JavaScript", "angularjs"); 44 | }); 45 | }); 46 | ``` 47 | 48 | Once that's done, you can use the Exceptionless client anywhere in your app by 49 | importing `$ExceptionlessClient` followed by the method you want to use. For example: 50 | 51 | ```js 52 | await $ExceptionlessClient.submitLog("Hello world!"); 53 | ``` 54 | 55 | Please see the [docs](https://exceptionless.com/docs/clients/javascript/) for 56 | more information on configuring the client. 57 | 58 | ## Support 59 | 60 | If you need help, please contact us via in-app support, 61 | [open an issue](https://github.com/exceptionless/Exceptionless.JavaScript/issues/new) 62 | or [join our chat on Discord](https://discord.gg/6HxgFCx). We’re always here to 63 | help if you have any questions! 64 | -------------------------------------------------------------------------------- /packages/angularjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exceptionless/angularjs", 3 | "version": "3.0.0-dev", 4 | "description": "JavaScript client for Exceptionless", 5 | "author": { 6 | "name": "exceptionless", 7 | "url": "https://exceptionless.com" 8 | }, 9 | "keywords": [ 10 | "exceptionless", 11 | "error", 12 | "feature", 13 | "logging", 14 | "tracking", 15 | "reporting", 16 | "angularjs" 17 | ], 18 | "repository": { 19 | "url": "git://github.com/exceptionless/Exceptionless.JavaScript.git", 20 | "type": "git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/exceptionless/Exceptionless.JavaScript/issues" 24 | }, 25 | "license": "Apache-2.0", 26 | "type": "module", 27 | "main": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "unpkg": "dist/index.bundle.min.js", 30 | "jsdelivr": "dist/index.bundle.min.js", 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./package.json": "./package.json" 34 | }, 35 | "scripts": { 36 | "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.min.js", 37 | "watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --watch --outfile=dist/index.bundle.js" 38 | }, 39 | "sideEffects": false, 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "devDependencies": { 44 | "@types/angular": "^1.8.9", 45 | "esbuild": "^0.20.2" 46 | }, 47 | "dependencies": { 48 | "@exceptionless/browser": "3.0.0-dev" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/angularjs/src/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserExceptionlessClient, Exceptionless, toError } from "@exceptionless/browser"; 2 | 3 | declare let angular; 4 | angular 5 | .module("exceptionless", []) 6 | .constant("$ExceptionlessClient", Exceptionless) 7 | .factory("exceptionlessHttpInterceptor", [ 8 | "$location", 9 | "$q", 10 | "$ExceptionlessClient", 11 | ($location: ng.ILocationService, $q: ng.IQService, $ExceptionlessClient: BrowserExceptionlessClient) => { 12 | return { 13 | responseError: function responseError(response: ng.IHttpResponse<{ Message?: string }>) { 14 | if (response.status === 404) { 15 | void $ExceptionlessClient.submitNotFound(response.config.url); 16 | } else if (response.status !== 401) { 17 | const message = `[${response.status}] ${response.data?.Message ?? response.config.url}`; 18 | void $ExceptionlessClient 19 | .createUnhandledException(new Error(message), "errorHttpInterceptor") 20 | .setSource(response.config.url) 21 | .setProperty("response", response) 22 | .setProperty("referrer", $location.absUrl()) 23 | .submit(); 24 | } 25 | return $q.reject(response); 26 | } 27 | }; 28 | } 29 | ]) 30 | .config([ 31 | "$httpProvider", 32 | "$provide", 33 | "$ExceptionlessClient", 34 | ($httpProvider: ng.IHttpProvider, $provide: ng.IModule, $ExceptionlessClient: BrowserExceptionlessClient) => { 35 | $httpProvider.interceptors.push("exceptionlessHttpInterceptor"); 36 | $provide.decorator("$exceptionHandler", [ 37 | "$delegate", 38 | ($delegate: (ex: Error, cause: string) => void) => { 39 | return (exception: Error, cause: string) => { 40 | $delegate(exception, cause); 41 | void $ExceptionlessClient.createUnhandledException(exception, "$exceptionHandler").setMessage(cause).submit(); 42 | }; 43 | } 44 | ]); 45 | $provide.decorator("$log", [ 46 | "$delegate", 47 | ($delegate) => { 48 | function decorateRegularCall(property: string, logLevel: string) { 49 | const previousFn = $delegate[property]; 50 | return ($delegate[property] = (...args: string[]) => { 51 | if ((angular as { mock?: unknown }).mock) { 52 | $delegate[property].logs = []; 53 | } 54 | 55 | // eslint-disable-next-line prefer-spread 56 | previousFn.apply(null, args); 57 | if (args[0] && args[0].length > 0) { 58 | void $ExceptionlessClient.submitLog(undefined, args[0], logLevel); 59 | } 60 | }); 61 | } 62 | 63 | $delegate.log = decorateRegularCall("log", "Trace"); 64 | $delegate.info = decorateRegularCall("info", "Info"); 65 | $delegate.warn = decorateRegularCall("warn", "Warn"); 66 | $delegate.debug = decorateRegularCall("debug", "Debug"); 67 | $delegate.error = decorateRegularCall("error", "Error"); 68 | return $delegate; 69 | } 70 | ]); 71 | } 72 | ]) 73 | .run([ 74 | "$rootScope", 75 | "$ExceptionlessClient", 76 | ($rootScope: ng.IRootScopeService, $ExceptionlessClient: BrowserExceptionlessClient) => { 77 | $rootScope.$on("$routeChangeSuccess", (_event, next, current) => { 78 | if (!current) { 79 | return; 80 | } 81 | 82 | void $ExceptionlessClient 83 | .createFeatureUsage(current.name as string) 84 | .setProperty("next", next) 85 | .setProperty("current", current) 86 | .submit(); 87 | }); 88 | 89 | $rootScope.$on("$routeChangeError", (_event, current, previous, rejection) => { 90 | const error: Error = toError(rejection, "Route Change Error"); 91 | void $ExceptionlessClient 92 | .createUnhandledException(error, "$routeChangeError") 93 | .setProperty("current", current) 94 | .setProperty("previous", previous) 95 | .submit(); 96 | }); 97 | 98 | $rootScope.$on("$stateChangeSuccess", (_event, toState, toParams, fromState, fromParams) => { 99 | if (!toState || toState.name === "otherwise") { 100 | return; 101 | } 102 | 103 | void $ExceptionlessClient 104 | .createFeatureUsage((toState.controller || toState.name) as string) 105 | .setProperty("toState", toState) 106 | .setProperty("toParams", toParams) 107 | .setProperty("fromState", fromState) 108 | .setProperty("fromParams", fromParams) 109 | .submit(); 110 | }); 111 | 112 | $rootScope.$on("$stateNotFound", (_event, unfoundState, fromState, fromParams) => { 113 | if (!unfoundState) { 114 | return; 115 | } 116 | 117 | void $ExceptionlessClient 118 | .createNotFound(unfoundState.to as string) 119 | .setProperty("unfoundState", unfoundState) 120 | .setProperty("fromState", fromState) 121 | .setProperty("fromParams", fromParams) 122 | .submit(); 123 | }); 124 | 125 | const stateChangeError = "$stateChangeError"; 126 | $rootScope.$on(stateChangeError, (_event, toState, toParams, fromState, fromParams, error) => { 127 | if (!error) { 128 | return; 129 | } 130 | 131 | const builder = 132 | error && error.status === 404 133 | ? $ExceptionlessClient.createNotFound(error.config.url as string) 134 | : $ExceptionlessClient.createUnhandledException(error as Error, stateChangeError); 135 | void builder 136 | .setSource(stateChangeError) 137 | .setMessage(error?.statusText as string) 138 | .setProperty("toState", toState) 139 | .setProperty("toParams", toParams) 140 | .setProperty("fromState", fromState) 141 | .setProperty("fromParams", fromParams) 142 | .submit(); 143 | }); 144 | 145 | $rootScope.$on("$destroy", () => { 146 | void $ExceptionlessClient.config.services.queue.process(); 147 | }); 148 | } 149 | ]); 150 | -------------------------------------------------------------------------------- /packages/angularjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false, 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "strict": false, 8 | "strictNullChecks": true 9 | }, 10 | "include": ["src"], 11 | "types": ["angular", "angular-mock"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/browser/README.md: -------------------------------------------------------------------------------- 1 | # Exceptionless Browser 2 | 3 | This package provides native JavaScript support for applications that are built 4 | in vanilla HTML and JS. 5 | 6 | ## Getting Started 7 | 8 | To use this package, your must be using ES6 and support ESM modules. 9 | 10 | ## Installation 11 | 12 | You can install Exceptionless either in your browser application using a `script` 13 | tag, or you can use the Node Package Manager (npm) to install the package. 14 | 15 | ### CDN 16 | 17 | Add the following script tag at the very beginning of your page: 18 | 19 | ```html 20 | 27 | ``` 28 | 29 | ### npm 30 | 31 | 1. Install the package by running `npm install @exceptionless/browser --save`. 32 | 2. Import Exceptionless and call startup during app startup. 33 | 34 | ```js 35 | import { Exceptionless } from "@exceptionless/browser"; 36 | 37 | await Exceptionless.startup((c) => { 38 | c.apiKey = "API_KEY_HERE"; 39 | }); 40 | ``` 41 | 42 | ## Configuration 43 | 44 | While your app is starting up, you should call `startup` on the Exceptionless 45 | client. This ensures the client is configured and automatic capturing of 46 | unhandled errors occurs. 47 | 48 | ```js 49 | import { Exceptionless } from "@exceptionless/browser"; 50 | 51 | await Exceptionless.startup((c) => { 52 | c.apiKey = "API_KEY_HERE"; 53 | c.setUserIdentity("12345678", "Blake"); 54 | c.useSessions(); 55 | 56 | // set some default data 57 | c.defaultData["mydata"] = { 58 | myGreeting: "Hello World" 59 | }; 60 | 61 | c.defaultTags.push("Example", "JavaScript", "Browser"); 62 | }); 63 | ``` 64 | 65 | Once that's done, you can use the Exceptionless client anywhere in your app by 66 | importing `Exceptionless` followed by the method you want to use. For example: 67 | 68 | ```js 69 | await Exceptionless.submitLog("Hello world!"); 70 | ``` 71 | 72 | Please see the [docs](https://exceptionless.com/docs/clients/javascript/) for 73 | more information on configuring the client. 74 | 75 | ## Support 76 | 77 | If you need help, please contact us via in-app support, 78 | [open an issue](https://github.com/exceptionless/Exceptionless.JavaScript/issues/new) 79 | or [join our chat on Discord](https://discord.gg/6HxgFCx). We’re always here to 80 | help if you have any questions! 81 | -------------------------------------------------------------------------------- /packages/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exceptionless/browser", 3 | "version": "3.0.0-dev", 4 | "description": "JavaScript client for Exceptionless", 5 | "author": { 6 | "name": "exceptionless", 7 | "url": "https://exceptionless.com" 8 | }, 9 | "keywords": [ 10 | "exceptionless", 11 | "error", 12 | "feature", 13 | "logging", 14 | "tracking", 15 | "reporting", 16 | "browser" 17 | ], 18 | "repository": { 19 | "url": "git://github.com/exceptionless/Exceptionless.JavaScript.git", 20 | "type": "git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/exceptionless/Exceptionless.JavaScript/issues" 24 | }, 25 | "license": "Apache-2.0", 26 | "type": "module", 27 | "main": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "unpkg": "dist/index.bundle.min.js", 30 | "jsdelivr": "dist/index.bundle.min.js", 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./package.json": "./package.json" 34 | }, 35 | "jest": { 36 | "moduleFileExtensions": [ 37 | "js", 38 | "ts" 39 | ], 40 | "moduleNameMapper": { 41 | "^@exceptionless/(.*)$": "/../$1/src" 42 | }, 43 | "preset": "ts-jest", 44 | "resolver": "jest-ts-webcompat-resolver", 45 | "testEnvironment": "jsdom" 46 | }, 47 | "scripts": { 48 | "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.min.js", 49 | "watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --watch --outfile=dist/index.bundle.js", 50 | "test": "jest", 51 | "test:watch": "jest --watch" 52 | }, 53 | "sideEffects": false, 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "devDependencies": { 58 | "@jest/globals": "^29.7.0", 59 | "esbuild": "^0.20.2", 60 | "jest": "^29.7.0", 61 | "jest-environment-jsdom": "^29.7.0", 62 | "jest-ts-webcompat-resolver": "^1.0.0", 63 | "ts-jest": "^29.1.2" 64 | }, 65 | "dependencies": { 66 | "@exceptionless/core": "3.0.0-dev", 67 | "stacktrace-js": "^2.0.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/browser/src/BrowserExceptionlessClient.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, ExceptionlessClient, SimpleErrorPlugin } from "@exceptionless/core"; 2 | 3 | import { BrowserErrorPlugin } from "./plugins/BrowserErrorPlugin.js"; 4 | import { BrowserGlobalHandlerPlugin } from "./plugins/BrowserGlobalHandlerPlugin.js"; 5 | import { BrowserIgnoreExtensionErrorsPlugin } from "./plugins/BrowserIgnoreExtensionErrorsPlugin.js"; 6 | import { BrowserLifeCyclePlugin } from "./plugins/BrowserLifeCyclePlugin.js"; 7 | import { BrowserModuleInfoPlugin } from "./plugins/BrowserModuleInfoPlugin.js"; 8 | import { BrowserRequestInfoPlugin } from "./plugins/BrowserRequestInfoPlugin.js"; 9 | 10 | export class BrowserExceptionlessClient extends ExceptionlessClient { 11 | public async startup(configurationOrApiKey?: (config: Configuration) => void | string): Promise { 12 | const config = this.config; 13 | if (configurationOrApiKey && !this._initialized) { 14 | config.useLocalStorage(); 15 | 16 | config.addPlugin(new BrowserGlobalHandlerPlugin()); 17 | config.addPlugin(new BrowserIgnoreExtensionErrorsPlugin()); 18 | config.addPlugin(new BrowserLifeCyclePlugin()); 19 | config.addPlugin(new BrowserModuleInfoPlugin()); 20 | config.addPlugin(new BrowserRequestInfoPlugin()); 21 | config.addPlugin(new BrowserErrorPlugin()); 22 | config.removePlugin(new SimpleErrorPlugin()); 23 | } 24 | 25 | await super.startup(configurationOrApiKey); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/browser/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@exceptionless/core"; 2 | 3 | export { BrowserErrorPlugin } from "./plugins/BrowserErrorPlugin.js"; 4 | export { BrowserGlobalHandlerPlugin } from "./plugins/BrowserGlobalHandlerPlugin.js"; 5 | export { BrowserIgnoreExtensionErrorsPlugin } from "./plugins/BrowserIgnoreExtensionErrorsPlugin.js"; 6 | export { BrowserLifeCyclePlugin } from "./plugins/BrowserLifeCyclePlugin.js"; 7 | export { BrowserModuleInfoPlugin } from "./plugins/BrowserModuleInfoPlugin.js"; 8 | export { BrowserRequestInfoPlugin } from "./plugins/BrowserRequestInfoPlugin.js"; 9 | export { BrowserExceptionlessClient } from "./BrowserExceptionlessClient.js"; 10 | 11 | import { BrowserExceptionlessClient } from "./BrowserExceptionlessClient.js"; 12 | export const Exceptionless = new BrowserExceptionlessClient(); 13 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/BrowserErrorPlugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorInfo, 3 | EventPluginContext, 4 | IEventPlugin, 5 | IgnoredErrorProperties, 6 | KnownEventDataKeys, 7 | ParameterInfo, 8 | StackFrameInfo, 9 | stringify, 10 | isEmpty 11 | } from "@exceptionless/core"; 12 | 13 | import { fromError, StackFrame } from "stacktrace-js"; 14 | 15 | export class BrowserErrorPlugin implements IEventPlugin { 16 | public priority = 30; 17 | public name = "BrowserErrorPlugin"; 18 | 19 | public async run(context: EventPluginContext): Promise { 20 | const exception = context.eventContext.getException(); 21 | if (exception) { 22 | if (!context.event.type) { 23 | context.event.type = "error"; 24 | } 25 | 26 | if (context.event.data && !context.event.data[KnownEventDataKeys.Error]) { 27 | const result = await this.parse(exception); 28 | if (result) { 29 | const exclusions = context.client.config.dataExclusions.concat(IgnoredErrorProperties); 30 | const additionalData = stringify(exception, exclusions); 31 | if (!isEmpty(additionalData)) { 32 | if (!result.data) { 33 | result.data = {}; 34 | } 35 | 36 | result.data["@ext"] = JSON.parse(additionalData); 37 | } 38 | 39 | context.event.data[KnownEventDataKeys.Error] = result; 40 | } 41 | } 42 | } 43 | } 44 | 45 | public async parse(exception: Error): Promise { 46 | function getParameters(parameters: string | string[]): ParameterInfo[] { 47 | const params: string[] = (typeof parameters === "string" ? [parameters] : parameters) || []; 48 | 49 | const items: ParameterInfo[] = []; 50 | for (const param of params) { 51 | items.push({ name: param }); 52 | } 53 | 54 | return items; 55 | } 56 | 57 | function getStackFrames(stackFrames: StackFrame[]): StackFrameInfo[] { 58 | const ANONYMOUS: string = ""; 59 | const frames: StackFrameInfo[] = []; 60 | 61 | for (const frame of stackFrames) { 62 | const fileName: string = frame.getFileName(); 63 | frames.push({ 64 | name: (frame.getFunctionName() || ANONYMOUS).replace("?", ANONYMOUS), 65 | parameters: getParameters(frame.getArgs()), 66 | file_name: fileName, 67 | line_number: frame.getLineNumber() || 0, 68 | column: frame.getColumnNumber() || 0, 69 | data: { 70 | is_native: frame.getIsNative() || (fileName && fileName[0] !== "/" && fileName[0] !== ".") 71 | } 72 | }); 73 | } 74 | 75 | return frames; 76 | } 77 | 78 | const result: StackFrame[] = exception.stack ? await fromError(exception) : []; 79 | if (!result) { 80 | throw new Error("Unable to parse the exception stack trace"); 81 | } 82 | 83 | // TODO: Test with reference error. 84 | return Promise.resolve({ 85 | type: exception.name || "Error", 86 | message: exception.message, 87 | stack_trace: getStackFrames(result || []) 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/BrowserGlobalHandlerPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient, IEventPlugin, PluginContext, toError } from "@exceptionless/core"; 2 | 3 | declare let $: (document: Document) => { 4 | ajaxError: { 5 | (document: (event: Event, xhr: { responseText: string; status: number }, settings: { data: unknown; url: string }, error: string) => void): void; 6 | }; 7 | }; 8 | 9 | export class BrowserGlobalHandlerPlugin implements IEventPlugin { 10 | public priority: number = 100; 11 | public name: string = "BrowserGlobalHandlerPlugin"; 12 | 13 | private _client: ExceptionlessClient | null = null; 14 | 15 | public startup(context: PluginContext): Promise { 16 | if (this._client || typeof window !== "object") { 17 | return Promise.resolve(); 18 | } 19 | 20 | this._client = context.client; 21 | 22 | // TODO: Discus if we want to unwire this handler in suspend? 23 | window.addEventListener("error", (event) => { 24 | void this._client?.submitUnhandledException(this.getError(event), "onerror"); 25 | }); 26 | 27 | window.addEventListener("unhandledrejection", (event) => { 28 | let reason: unknown = event.reason; 29 | if (!(reason instanceof Error)) { 30 | try { 31 | // Check for reason in legacy CustomEvents (https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) 32 | const detailReason = (<{ detail?: { reason: string } }>event).detail?.reason; 33 | if (detailReason) { 34 | reason = detailReason; 35 | } 36 | } catch (ex) { 37 | /* empty */ 38 | } 39 | } 40 | 41 | if (!(reason instanceof Error)) { 42 | try { 43 | const error = event.reason.error; 44 | if (error) { 45 | reason = error; 46 | } 47 | } catch (ex) { 48 | /* empty */ 49 | } 50 | } 51 | 52 | const error: Error = toError(reason, "Unhandled rejection"); 53 | void this._client?.submitUnhandledException(error, "onunhandledrejection"); 54 | }); 55 | 56 | if (typeof $ !== "undefined" && $(document)) { 57 | $(document).ajaxError((_: Event, xhr: { responseText: string; status: number }, settings: { data: unknown; url: string }, error: string) => { 58 | if (xhr.status === 404) { 59 | // TODO: Handle async 60 | void this._client?.submitNotFound(settings.url); 61 | } else if (xhr.status !== 401) { 62 | // TODO: Handle async 63 | void this._client 64 | ?.createUnhandledException(toError(error), "JQuery.ajaxError") 65 | .setSource(settings.url) 66 | .setProperty("status", xhr.status) 67 | .setProperty("request", settings.data) 68 | .setProperty("response", xhr.responseText?.slice(0, 1024)) 69 | .submit(); 70 | } 71 | }); 72 | } 73 | 74 | return Promise.resolve(); 75 | } 76 | 77 | private getError(event: ErrorEvent): Error { 78 | const { error, message, filename, lineno, colno } = event; 79 | if (typeof error === "object") { 80 | return error as Error; 81 | } 82 | 83 | let name: string = "Error"; 84 | let msg: string = message || event.error; 85 | if (msg) { 86 | const errorNameRegex: RegExp = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Aggregate|Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/i; 87 | const regexResult = errorNameRegex.exec(msg); 88 | if (regexResult) { 89 | const [, errorName, errorMessage] = regexResult; 90 | if (errorName) { 91 | name = errorName; 92 | } 93 | if (errorMessage) { 94 | msg = errorMessage; 95 | } 96 | } 97 | } 98 | 99 | const ex = new Error(msg || "Script error."); 100 | ex.name = name; 101 | ex.stack = `at ${filename || ""}:${!isNaN(lineno) ? lineno : 0}${!isNaN(colno) ? `:${colno}` : ""}`; 102 | return ex; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/BrowserIgnoreExtensionErrorsPlugin.ts: -------------------------------------------------------------------------------- 1 | import { EventPluginContext, IEventPlugin } from "@exceptionless/core"; 2 | 3 | export class BrowserIgnoreExtensionErrorsPlugin implements IEventPlugin { 4 | public priority = 15; 5 | public name = "BrowserIgnoreExtensionErrorsPlugin"; 6 | 7 | public async run(context: EventPluginContext): Promise { 8 | const exception = context.eventContext.getException(); 9 | if (exception?.stack && exception.stack.includes("-extension://")) { 10 | // Handles all extensions like chrome-extension://, moz-extension://, ms-browser-extension://, safari-extension:// 11 | context.log.info("Ignoring event with error stack containing browser extension"); 12 | context.cancelled = true; 13 | } 14 | 15 | return Promise.resolve(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/BrowserLifeCyclePlugin.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient, IEventPlugin, PluginContext } from "@exceptionless/core"; 2 | 3 | export class BrowserLifeCyclePlugin implements IEventPlugin { 4 | public priority: number = 105; 5 | public name: string = "BrowserLifeCyclePlugin"; 6 | 7 | private _client: ExceptionlessClient | null = null; 8 | 9 | public startup(context: PluginContext): Promise { 10 | if (this._client || typeof document !== "object") { 11 | return Promise.resolve(); 12 | } 13 | 14 | this._client = context.client; 15 | 16 | globalThis.addEventListener("beforeunload", () => { 17 | if (this._client?.config.sessionsEnabled) { 18 | void this._client?.submitSessionEnd(); 19 | } 20 | 21 | void this._client?.suspend(); 22 | }); 23 | 24 | document.addEventListener("visibilitychange", () => { 25 | if (document.visibilityState === "visible") { 26 | void this._client?.startup(); 27 | } else { 28 | void this._client?.suspend(); 29 | } 30 | }); 31 | 32 | return Promise.resolve(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/BrowserModuleInfoPlugin.ts: -------------------------------------------------------------------------------- 1 | import { EventPluginContext, getHashCode, IEventPlugin, KnownEventDataKeys, ModuleInfo, parseVersion } from "@exceptionless/core"; 2 | 3 | export class BrowserModuleInfoPlugin implements IEventPlugin { 4 | public priority: number = 50; 5 | public name: string = "BrowserModuleInfoPlugin"; 6 | private _modules: ModuleInfo[] | undefined; 7 | 8 | public startup(): Promise { 9 | if (!this._modules) { 10 | this._modules = this.getModules(); 11 | } 12 | 13 | return Promise.resolve(); 14 | } 15 | 16 | public run(context: EventPluginContext): Promise { 17 | const error = context.event.data?.[KnownEventDataKeys.Error]; 18 | if (error && !error?.modules && this._modules?.length) { 19 | error.modules = this._modules; 20 | } 21 | 22 | return Promise.resolve(); 23 | } 24 | 25 | private getModules(): ModuleInfo[] | undefined { 26 | if (typeof document !== "object" || !document.getElementsByTagName) { 27 | return; 28 | } 29 | 30 | const modules: ModuleInfo[] = []; 31 | const scripts: HTMLCollectionOf = document.getElementsByTagName("script"); 32 | if (scripts && scripts.length > 0) { 33 | for (let index = 0; index < scripts.length; index++) { 34 | if (scripts[index].src) { 35 | modules.push({ 36 | module_id: index, 37 | name: scripts[index].src.split("?")[0], 38 | version: parseVersion(scripts[index].src) 39 | }); 40 | } else if (scripts[index].innerHTML) { 41 | modules.push({ 42 | module_id: index, 43 | name: "Script Tag", 44 | version: getHashCode(scripts[index].innerHTML).toString() 45 | }); 46 | } 47 | } 48 | } 49 | 50 | return modules; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/BrowserRequestInfoPlugin.ts: -------------------------------------------------------------------------------- 1 | import { EventPluginContext, getCookies, IEventPlugin, isMatch, KnownEventDataKeys, parseQueryString, RequestInfo } from "@exceptionless/core"; 2 | 3 | export class BrowserRequestInfoPlugin implements IEventPlugin { 4 | public priority: number = 70; 5 | public name: string = "BrowserRequestInfoPlugin"; 6 | 7 | public run(context: EventPluginContext): Promise { 8 | if (context.event.data && !context.event.data[KnownEventDataKeys.RequestInfo]) { 9 | const requestInfo: RequestInfo | undefined = this.getRequestInfo(context); 10 | if (requestInfo) { 11 | if (isMatch(requestInfo.user_agent, context.client.config.userAgentBotPatterns)) { 12 | context.log.info("Cancelling event as the request user agent matches a known bot pattern"); 13 | context.cancelled = true; 14 | } else { 15 | context.event.data[KnownEventDataKeys.RequestInfo] = requestInfo; 16 | } 17 | } 18 | } 19 | 20 | return Promise.resolve(); 21 | } 22 | 23 | private getRequestInfo(context: EventPluginContext): RequestInfo | undefined { 24 | if (typeof document !== "object" || typeof navigator !== "object" || typeof location !== "object") { 25 | return; 26 | } 27 | 28 | const config = context.client.config; 29 | const exclusions = config.dataExclusions; 30 | const requestInfo: RequestInfo = { 31 | user_agent: navigator.userAgent, 32 | is_secure: location.protocol === "https:", 33 | host: location.hostname, 34 | port: location.port && location.port !== "" ? parseInt(location.port, 10) : 80, 35 | path: location.pathname 36 | }; 37 | 38 | if (config.includeCookies) { 39 | requestInfo.cookies = getCookies(document.cookie, exclusions) as Record; 40 | } 41 | 42 | if (config.includeQueryString) { 43 | requestInfo.query_string = parseQueryString(location.search.substring(1), exclusions); 44 | } 45 | 46 | if (document.referrer && document.referrer !== "") { 47 | requestInfo.referrer = document.referrer; 48 | } 49 | 50 | return requestInfo; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/browser/test/plugins/BrowserErrorPlugin.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { ErrorInfo, Event, EventContext, EventPluginContext, ExceptionlessClient, KnownEventDataKeys } from "@exceptionless/core"; 5 | 6 | import { CapturedExceptions } from "./../../../core/test/plugins/default/exceptions.js"; 7 | import { BrowserErrorPlugin } from "../../src/plugins/BrowserErrorPlugin.js"; 8 | 9 | class BaseTestError extends Error { 10 | public name = "NotImplementedError"; 11 | public someProperty = "Test"; 12 | } 13 | 14 | class DerivedTestError extends BaseTestError { 15 | public someOtherProperty = "Test2"; 16 | } 17 | 18 | describe("BrowserErrorPlugin", () => { 19 | const plugin = new BrowserErrorPlugin(); 20 | let context: EventPluginContext; 21 | 22 | beforeEach(() => { 23 | plugin.parse = (exception: Error): Promise => { 24 | return Promise.resolve({ 25 | type: exception.name, 26 | message: exception.message, 27 | stack_trace: [], 28 | modules: [] 29 | }); 30 | }; 31 | 32 | const client: ExceptionlessClient = new ExceptionlessClient(); 33 | const event: Event = { 34 | data: {} 35 | }; 36 | 37 | context = new EventPluginContext(client, event, new EventContext()); 38 | }); 39 | 40 | function processError(error: Error | string | unknown): Promise { 41 | const exception = throwAndCatch(error); 42 | context.eventContext.setException(exception); 43 | return plugin.run(context); 44 | } 45 | 46 | describe("additional data", () => { 47 | describeForCapturedExceptions((exception) => { 48 | test("should ignore default error properties", async () => { 49 | context.eventContext.setException(exception as Error); 50 | await plugin.run(context); 51 | const additionalData = getAdditionalData(context.event); 52 | expect(additionalData).toBeUndefined(); 53 | }); 54 | }); 55 | 56 | test("should add error cause", async () => { 57 | const error = { 58 | someProperty: "Test" 59 | }; 60 | await processError(new Error("Error With Cause", { cause: error })); 61 | const additionalData = getAdditionalData(context.event); 62 | expect(additionalData).not.toBeNull(); 63 | expect(additionalData?.cause).toStrictEqual(error); 64 | }); 65 | 66 | test("should add custom properties to additional data", async () => { 67 | const error = { 68 | someProperty: "Test" 69 | }; 70 | await processError(error); 71 | const additionalData = getAdditionalData(context.event); 72 | expect(additionalData).not.toBeNull(); 73 | expect(additionalData?.someProperty).toBe("Test"); 74 | }); 75 | 76 | test("should support custom exception types", async () => { 77 | await processError(new BaseTestError()); 78 | const additionalData = getAdditionalData(context.event); 79 | expect(additionalData).not.toBeNull(); 80 | expect(additionalData?.someProperty).toBe("Test"); 81 | }); 82 | 83 | test("should support inherited properties", async () => { 84 | await processError(new DerivedTestError()); 85 | const additionalData = getAdditionalData(context.event); 86 | expect(additionalData).not.toBeNull(); 87 | expect(additionalData?.someProperty).toBe("Test"); 88 | expect(additionalData?.someOtherProperty).toBe("Test2"); 89 | }); 90 | 91 | test("shouldn't set empty additional data", async () => { 92 | await processError({}); 93 | const additionalData = getAdditionalData(context.event); 94 | expect(additionalData).toBeUndefined(); 95 | }); 96 | 97 | test("should ignore functions", async () => { 98 | class ErrorWithFunction extends Error { 99 | constructor() { 100 | super("Error with function"); 101 | } 102 | 103 | public someFunction(): number { 104 | return 5; 105 | } 106 | } 107 | const exception = new ErrorWithFunction(); 108 | context.eventContext.setException(exception); 109 | 110 | await plugin.run(context); 111 | 112 | const additionalData = getAdditionalData(context.event); 113 | expect(additionalData).toBeUndefined(); 114 | }); 115 | }); 116 | }); 117 | 118 | function describeForCapturedExceptions(specDefinitions: (exception: Error | unknown) => void) { 119 | const exceptions = >CapturedExceptions; 120 | const keys = Object.getOwnPropertyNames(exceptions); 121 | keys.forEach((key) => { 122 | describe(key, () => specDefinitions(exceptions[key])); 123 | }); 124 | } 125 | 126 | function getError(event: Event): ErrorInfo | undefined { 127 | return event?.data?.[KnownEventDataKeys.Error]; 128 | } 129 | 130 | function getAdditionalData(event: Event): Record | undefined { 131 | const error = getError(event); 132 | return error?.data?.["@ext"] as Record; 133 | } 134 | 135 | function throwAndCatch(error: Error | string | unknown): Error { 136 | try { 137 | throw error; 138 | } catch (exception) { 139 | return exception as Error; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/browser/test/plugins/BrowserIgnoreExtensionErrorsPlugin.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { EventContext, EventPluginContext, ExceptionlessClient } from "@exceptionless/core"; 5 | 6 | import { BrowserIgnoreExtensionErrorsPlugin } from "../../src/plugins/BrowserIgnoreExtensionErrorsPlugin.js"; 7 | 8 | describe("BrowserIgnoreExtensionErrorsPlugin", () => { 9 | let client: ExceptionlessClient; 10 | let plugin: BrowserIgnoreExtensionErrorsPlugin; 11 | 12 | beforeEach(() => { 13 | client = new ExceptionlessClient(); 14 | plugin = new BrowserIgnoreExtensionErrorsPlugin(); 15 | }); 16 | 17 | const run = async (stackTrace?: string | undefined): Promise => { 18 | const error = new Error("Test"); 19 | if (stackTrace) { 20 | error.stack = stackTrace; 21 | } 22 | 23 | const eventContext = new EventContext(); 24 | eventContext.setException(error); 25 | 26 | const context = new EventPluginContext(client, { type: "error" }, eventContext); 27 | 28 | await plugin.run(context); 29 | return context; 30 | }; 31 | 32 | test("should not cancel empty stack trace", async () => { 33 | const context = await run(); 34 | expect(context.cancelled).toBe(false); 35 | }); 36 | 37 | test("should not cancel normal stack trace", async () => { 38 | const context = await run("at t() in https://test/Content/js/Exceptionless/exceptionless.min.js:line 1:col 260"); 39 | expect(context.cancelled).toBe(false); 40 | }); 41 | 42 | test("should cancel browser extension stack trace", async () => { 43 | const context = await run("at Object.initialize() in chrome-extension://bmagokdooijbeehmkpknfglimnifench/firebug-lite.js:line 6289:col 29"); 44 | expect(context.cancelled).toBe(true); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ES2022"], 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "types": ["jest"] 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Exceptionless Client SDK 2 | 3 | This package contains the default implementations that all other JS clients 4 | build upon. You can use it directly but we recommend using one of the packages 5 | below which automatically wire up to platform specific error handlers and 6 | platform specific plugins. 7 | 8 | - [`@exceptionless/angularjs`](https://github.com/exceptionless/Exceptionless.JavaScript/tree/master/packages/angularjs) 9 | - [`@exceptionless/browser`](https://github.com/exceptionless/Exceptionless.JavaScript/tree/master/packages/browser) 10 | - [`@exceptionless/node`](https://github.com/exceptionless/Exceptionless.JavaScript/tree/master/packages/node) 11 | - [`@exceptionless/react`](https://github.com/exceptionless/Exceptionless.JavaScript/tree/master/packages/react) 12 | - [`@exceptionless/vue`](https://github.com/exceptionless/Exceptionless.JavaScript/tree/master/packages/vue) 13 | 14 | ## Getting Started 15 | 16 | To use this package, your must be using ES6 and support ESM modules. 17 | 18 | ## Installation 19 | 20 | `npm install @exceptionless/core --save` 21 | 22 | ## Configuration 23 | 24 | While your app is starting up, you should call `startup` on the Exceptionless 25 | client. This ensures the client is configured and automatic capturing of 26 | unhandled errors occurs. 27 | 28 | ```js 29 | import { Exceptionless } from "@exceptionless/core"; 30 | 31 | await Exceptionless.startup((c) => { 32 | c.apiKey = "API_KEY_HERE"; 33 | c.setUserIdentity("12345678", "Blake"); 34 | 35 | // set some default data 36 | c.defaultData["mydata"] = { 37 | myGreeting: "Hello World" 38 | }; 39 | 40 | c.defaultTags.push("Example", "JavaScript"); 41 | }); 42 | ``` 43 | 44 | Once that's done, you can use the Exceptionless client anywhere in your app by 45 | importing `Exceptionless` followed by the method you want to use. For example: 46 | 47 | ```js 48 | await Exceptionless.submitLog("Hello world!"); 49 | ``` 50 | 51 | Please see the [docs](https://exceptionless.com/docs/clients/javascript/) for 52 | more information on configuring the client. 53 | 54 | ## Support 55 | 56 | If you need help, please contact us via in-app support, 57 | [open an issue](https://github.com/exceptionless/Exceptionless.JavaScript/issues/new) 58 | or [join our chat on Discord](https://discord.gg/6HxgFCx). We’re always here to 59 | help if you have any questions! 60 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exceptionless/core", 3 | "version": "3.0.0-dev", 4 | "description": "JavaScript client for Exceptionless", 5 | "author": { 6 | "name": "exceptionless", 7 | "url": "https://exceptionless.com" 8 | }, 9 | "homepage": "https://exceptionless.com", 10 | "keywords": [ 11 | "exceptionless", 12 | "error", 13 | "feature", 14 | "logging", 15 | "tracking", 16 | "reporting" 17 | ], 18 | "repository": { 19 | "url": "git://github.com/exceptionless/Exceptionless.JavaScript.git", 20 | "type": "git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/exceptionless/Exceptionless.JavaScript/issues" 24 | }, 25 | "license": "Apache-2.0", 26 | "type": "module", 27 | "main": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "unpkg": "dist/index.bundle.min.js", 30 | "jsdelivr": "dist/index.bundle.min.js", 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./package.json": "./package.json" 34 | }, 35 | "jest": { 36 | "moduleFileExtensions": [ 37 | "js", 38 | "ts" 39 | ], 40 | "moduleNameMapper": { 41 | "^@exceptionless/(.*)$": "/../$1/src" 42 | }, 43 | "preset": "ts-jest", 44 | "resolver": "jest-ts-webcompat-resolver", 45 | "testEnvironment": "jsdom" 46 | }, 47 | "scripts": { 48 | "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.min.js", 49 | "watch": "tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --watch --outfile=dist/index.bundle.js", 50 | "test": "jest", 51 | "test:watch": "jest --watch" 52 | }, 53 | "sideEffects": false, 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "devDependencies": { 58 | "@jest/globals": "^29.7.0", 59 | "esbuild": "^0.20.2", 60 | "jest": "^29.7.0", 61 | "jest-environment-jsdom": "^29.7.0", 62 | "jest-ts-webcompat-resolver": "^1.0.0", 63 | "ts-jest": "^29.1.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/EventBuilder.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient } from "./ExceptionlessClient.js"; 2 | import { Event, EventType, KnownEventDataKeys } from "./models/Event.js"; 3 | import { ManualStackingInfo } from "./models/data/ManualStackingInfo.js"; 4 | import { UserInfo } from "./models/data/UserInfo.js"; 5 | import { EventContext } from "./models/EventContext.js"; 6 | import { isEmpty, stringify } from "./Utils.js"; 7 | import { EventPluginContext } from "./plugins/EventPluginContext.js"; 8 | 9 | export class EventBuilder { 10 | public target: Event; 11 | public client: ExceptionlessClient; 12 | public context: EventContext; 13 | 14 | private _validIdentifierErrorMessage = "must contain between 8 and 100 alphanumeric or '-' characters."; 15 | 16 | constructor(event: Event, client: ExceptionlessClient, context?: EventContext) { 17 | this.target = event; 18 | this.client = client; 19 | this.context = context || new EventContext(); 20 | } 21 | 22 | public setType(type: EventType): EventBuilder { 23 | if (type) { 24 | this.target.type = type; 25 | } 26 | 27 | return this; 28 | } 29 | 30 | public setSource(source: string): EventBuilder { 31 | if (source) { 32 | this.target.source = source; 33 | } 34 | 35 | return this; 36 | } 37 | 38 | public setReferenceId(referenceId: string): EventBuilder { 39 | if (!this.isValidIdentifier(referenceId)) { 40 | throw new Error(`ReferenceId ${this._validIdentifierErrorMessage}`); 41 | } 42 | 43 | this.target.reference_id = referenceId; 44 | return this; 45 | } 46 | 47 | /** 48 | * Allows you to reference a parent event by its ReferenceId property. This allows you to have parent and child relationships. 49 | * @param name Reference name 50 | * @param id The reference id that points to a specific event 51 | */ 52 | public setEventReference(name: string, id: string): EventBuilder { 53 | if (!name) { 54 | throw new Error("Invalid name"); 55 | } 56 | 57 | if (!id || !this.isValidIdentifier(id)) { 58 | throw new Error(`Id ${this._validIdentifierErrorMessage}`); 59 | } 60 | 61 | this.setProperty("@ref:" + name, id); 62 | return this; 63 | } 64 | 65 | public setMessage(message: string | null | undefined): EventBuilder { 66 | if (message) { 67 | this.target.message = message; 68 | } 69 | 70 | return this; 71 | } 72 | 73 | public setGeo(latitude: number, longitude: number): EventBuilder { 74 | if (latitude < -90.0 || latitude > 90.0) { 75 | throw new Error("Must be a valid latitude value between -90.0 and 90.0."); 76 | } 77 | 78 | if (longitude < -180.0 || longitude > 180.0) { 79 | throw new Error("Must be a valid longitude value between -180.0 and 180.0."); 80 | } 81 | 82 | this.target.geo = `${latitude},${longitude}`; 83 | return this; 84 | } 85 | 86 | public setUserIdentity(userInfo: UserInfo): EventBuilder; 87 | public setUserIdentity(identity: string): EventBuilder; 88 | public setUserIdentity(identity: string, name: string): EventBuilder; 89 | public setUserIdentity(userInfoOrIdentity: UserInfo | string, name?: string): EventBuilder { 90 | const userInfo = typeof userInfoOrIdentity !== "string" ? userInfoOrIdentity : { identity: userInfoOrIdentity, name }; 91 | if (!userInfo || (!userInfo.identity && !userInfo.name)) { 92 | return this; 93 | } 94 | 95 | this.setProperty(KnownEventDataKeys.UserInfo, userInfo); 96 | return this; 97 | } 98 | 99 | /** 100 | * Sets the user"s description of the event. 101 | * 102 | * @param emailAddress The email address 103 | * @param description The user"s description of the event. 104 | */ 105 | public setUserDescription(emailAddress: string, description: string): EventBuilder { 106 | if (emailAddress && description) { 107 | this.setProperty(KnownEventDataKeys.UserDescription, { 108 | email_address: emailAddress, 109 | description 110 | }); 111 | } 112 | 113 | return this; 114 | } 115 | 116 | /** 117 | * Changes default stacking behavior by setting manual 118 | * stacking information. 119 | * @param signatureData A dictionary of strings to use for stacking. 120 | * @param title An optional title for the stacking information. 121 | */ 122 | public setManualStackingInfo(signatureData: Record, title?: string): EventBuilder { 123 | if (signatureData) { 124 | const stack: ManualStackingInfo = { signature_data: signatureData }; 125 | if (title) { 126 | stack.title = title; 127 | } 128 | 129 | this.setProperty(KnownEventDataKeys.ManualStackingInfo, stack); 130 | } 131 | 132 | return this; 133 | } 134 | 135 | /** 136 | * Changes default stacking behavior by setting the stacking key. 137 | * @param manualStackingKey The manual stacking key. 138 | * @param title An optional title for the stacking information. 139 | */ 140 | public setManualStackingKey(manualStackingKey: string, title?: string): EventBuilder { 141 | if (manualStackingKey) { 142 | const data = { ManualStackingKey: manualStackingKey }; 143 | this.setManualStackingInfo(data, title); 144 | } 145 | 146 | return this; 147 | } 148 | 149 | /** 150 | * Sets the event value. 151 | * @param value The value of the event. 152 | */ 153 | public setValue(value: number): EventBuilder { 154 | if (value) { 155 | this.target.value = value; 156 | } 157 | 158 | return this; 159 | } 160 | 161 | public addTags(...tags: string[]): EventBuilder { 162 | this.target.tags = [...(this.target.tags || []), ...tags]; 163 | return this; 164 | } 165 | 166 | /** 167 | * Adds the object to extended data. Uses @excludedPropertyNames 168 | * to exclude data from being included in the event. 169 | * @param name The name of the object to add. 170 | * @param value The data object to add. 171 | * @param maxDepth The max depth of the object to include. 172 | * @param excludedPropertyNames Any property names that should be excluded. 173 | */ 174 | public setProperty(name: string, value: unknown, maxDepth?: number, excludedPropertyNames?: string[]): EventBuilder { 175 | if (!name || value === undefined || value == null) { 176 | return this; 177 | } 178 | 179 | if (!this.target.data) { 180 | this.target.data = {}; 181 | } 182 | 183 | const exclusions = this.client.config.dataExclusions.concat(excludedPropertyNames || []); 184 | const json = stringify(value, exclusions, maxDepth); 185 | if (!isEmpty(json)) { 186 | this.target.data[name] = JSON.parse(json); 187 | } 188 | 189 | return this; 190 | } 191 | 192 | public setContextProperty(name: string, value: unknown): EventBuilder { 193 | this.context[name] = value; 194 | return this; 195 | } 196 | 197 | public markAsCritical(critical: boolean): EventBuilder { 198 | if (critical) { 199 | this.addTags("Critical"); 200 | } 201 | 202 | return this; 203 | } 204 | 205 | public submit(): Promise { 206 | return this.client.submitEvent(this.target, this.context); 207 | } 208 | 209 | private isValidIdentifier(value: string): boolean { 210 | if (!value) { 211 | return true; 212 | } 213 | 214 | if (value.length < 8 || value.length > 100) { 215 | return false; 216 | } 217 | 218 | for (let index = 0; index < value.length; index++) { 219 | const code = value.charCodeAt(index); 220 | const isDigit = code >= 48 && code <= 57; 221 | const isLetter = (code >= 65 && code <= 90) || (code >= 97 && code <= 122); 222 | const isMinus = code === 45; 223 | 224 | if (!(isDigit || isLetter) && !isMinus) { 225 | return false; 226 | } 227 | } 228 | 229 | return true; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /packages/core/src/configuration/SettingsManager.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "./Configuration.js"; 2 | 3 | export class ServerSettings { 4 | constructor( 5 | public settings: Record, 6 | public version: number 7 | ) {} 8 | } 9 | 10 | export class SettingsManager { 11 | private static readonly SettingsKey: string = "settings"; 12 | private static _isUpdatingSettings = false; 13 | 14 | public static async applySavedServerSettings(config: Configuration): Promise { 15 | if (!config?.isValid) { 16 | return; 17 | } 18 | 19 | const savedSettings = await this.getSavedServerSettings(config); 20 | if (savedSettings) { 21 | config.applyServerSettings(savedSettings); 22 | } 23 | } 24 | 25 | public static async updateSettings(config: Configuration): Promise { 26 | if (!config?.enabled || this._isUpdatingSettings) { 27 | return; 28 | } 29 | 30 | this._isUpdatingSettings = true; 31 | const { log, storage, submissionClient } = config.services; 32 | 33 | try { 34 | const unableToUpdateMessage = "Unable to update settings"; 35 | if (!config.isValid) { 36 | log.error(`${unableToUpdateMessage}: ApiKey is not set`); 37 | return; 38 | } 39 | 40 | const version = config.settingsVersion; 41 | log.trace(`Checking for updated settings from: v${version}`); 42 | const response = await submissionClient.getSettings(version); 43 | 44 | if (response.status === 304) { 45 | log.trace("Settings are up-to-date"); 46 | return; 47 | } 48 | 49 | if (!response?.success || !response.data) { 50 | log.warn(`${unableToUpdateMessage}: ${response.message}`); 51 | return; 52 | } 53 | 54 | config.applyServerSettings(response.data); 55 | 56 | await storage.setItem(SettingsManager.SettingsKey, JSON.stringify(response.data)); 57 | log.trace(`Updated settings: v${response.data.version}`); 58 | } catch (ex) { 59 | log.error(`Error updating settings: ${ex instanceof Error ? ex.message : ex + ""}`); 60 | } finally { 61 | this._isUpdatingSettings = false; 62 | } 63 | } 64 | 65 | private static async getSavedServerSettings(config: Configuration): Promise { 66 | const { log, storage } = config.services; 67 | try { 68 | const settings = await storage.getItem(SettingsManager.SettingsKey); 69 | return (settings && (JSON.parse(settings) as ServerSettings)) || new ServerSettings({}, 0); 70 | } catch (ex) { 71 | log.error(`Error getting saved settings: ${ex instanceof Error ? ex.message : ex + ""}`); 72 | return new ServerSettings({}, 0); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Configuration } from "./configuration/Configuration.js"; 2 | export { SettingsManager } from "./configuration/SettingsManager.js"; 3 | 4 | export { DefaultLastReferenceIdManager } from "./lastReferenceIdManager/DefaultLastReferenceIdManager.js"; 5 | export type { ILastReferenceIdManager } from "./lastReferenceIdManager/ILastReferenceIdManager.js"; 6 | 7 | export type { ILog } from "./logging/ILog.js"; 8 | export { ConsoleLog } from "./logging/ConsoleLog.js"; 9 | export { NullLog } from "./logging/NullLog.js"; 10 | 11 | export type { Event, EventType, IEventData, LogLevel } from "./models/Event.js"; 12 | export { KnownEventDataKeys } from "./models/Event.js"; 13 | export type { EnvironmentInfo } from "./models/data/EnvironmentInfo.js"; 14 | export type { ManualStackingInfo } from "./models/data/ManualStackingInfo.js"; 15 | export type { RequestInfo } from "./models/data/RequestInfo.js"; 16 | export type { UserDescription } from "./models/data/UserDescription.js"; 17 | export type { UserInfo } from "./models/data/UserInfo.js"; 18 | export type { ModuleInfo } from "./models/data/ModuleInfo.js"; 19 | 20 | export type { SimpleError, ErrorInfo, InnerErrorInfo, MethodInfo, ParameterInfo, StackFrameInfo } from "./models/data/ErrorInfo.js"; 21 | 22 | export { ConfigurationDefaultsPlugin } from "./plugins/default/ConfigurationDefaultsPlugin.js"; 23 | export { DuplicateCheckerPlugin } from "./plugins/default/DuplicateCheckerPlugin.js"; 24 | export { EventExclusionPlugin } from "./plugins/default/EventExclusionPlugin.js"; 25 | export { HeartbeatPlugin } from "./plugins/default/HeartbeatPlugin.js"; 26 | export { ReferenceIdPlugin } from "./plugins/default/ReferenceIdPlugin.js"; 27 | export { SessionIdManagementPlugin } from "./plugins/default/SessionIdManagementPlugin.js"; 28 | export { IgnoredErrorProperties, SimpleErrorPlugin } from "./plugins/default/SimpleErrorPlugin.js"; 29 | export { SubmissionMethodPlugin } from "./plugins/default/SubmissionMethodPlugin.js"; 30 | export { EventContext } from "./models/EventContext.js"; 31 | export { PluginContext } from "./plugins/PluginContext.js"; 32 | export { EventPluginContext } from "./plugins/EventPluginContext.js"; 33 | export { EventPluginManager } from "./plugins/EventPluginManager.js"; 34 | export type { IEventPlugin } from "./plugins/IEventPlugin.js"; 35 | 36 | export { DefaultEventQueue } from "./queue/DefaultEventQueue.js"; 37 | export type { IEventQueue } from "./queue/IEventQueue.js"; 38 | 39 | export { InMemoryStorage } from "./storage/InMemoryStorage.js"; 40 | export { LocalStorage } from "./storage/LocalStorage.js"; 41 | export type { IStorage } from "./storage/IStorage.js"; 42 | 43 | export type { ISubmissionClient } from "./submission/ISubmissionClient.js"; 44 | export { Response } from "./submission/Response.js"; 45 | export type { FetchOptions } from "./submission/DefaultSubmissionClient.js"; 46 | export { DefaultSubmissionClient } from "./submission/DefaultSubmissionClient.js"; 47 | export { EventBuilder } from "./EventBuilder.js"; 48 | export { ExceptionlessClient } from "./ExceptionlessClient.js"; 49 | 50 | export { 51 | endsWith, 52 | getCookies, 53 | getHashCode, 54 | guid, 55 | isEmpty, 56 | isMatch, 57 | parseQueryString, 58 | parseVersion, 59 | prune, 60 | randomNumber, 61 | startsWith, 62 | stringify, 63 | toBoolean, 64 | toError 65 | } from "./Utils.js"; 66 | -------------------------------------------------------------------------------- /packages/core/src/lastReferenceIdManager/DefaultLastReferenceIdManager.ts: -------------------------------------------------------------------------------- 1 | import { ILastReferenceIdManager } from "./ILastReferenceIdManager.js"; 2 | 3 | export class DefaultLastReferenceIdManager implements ILastReferenceIdManager { 4 | /** 5 | * Gets the last event's reference id that was submitted to the server. 6 | */ 7 | private _lastReferenceId: string | null = null; 8 | 9 | /** 10 | * Gets the last event's reference id that was submitted to the server. 11 | */ 12 | public getLast(): string | null { 13 | return this._lastReferenceId; 14 | } 15 | 16 | /** 17 | * Clears the last event's reference id. 18 | */ 19 | public clearLast(): void { 20 | this._lastReferenceId = null; 21 | } 22 | 23 | /** 24 | * Sets the last event's reference id. 25 | */ 26 | public setLast(eventId: string): void { 27 | this._lastReferenceId = eventId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/lastReferenceIdManager/ILastReferenceIdManager.ts: -------------------------------------------------------------------------------- 1 | export interface ILastReferenceIdManager { 2 | getLast(): string | null; 3 | clearLast(): void; 4 | setLast(eventId: string): void; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/logging/ConsoleLog.ts: -------------------------------------------------------------------------------- 1 | import { ILog } from "./ILog.js"; 2 | 3 | export class ConsoleLog implements ILog { 4 | public trace(message: string): void { 5 | this.log("debug", message); 6 | } 7 | 8 | public info(message: string): void { 9 | this.log("info", message); 10 | } 11 | 12 | public warn(message: string): void { 13 | this.log("warn", message); 14 | } 15 | 16 | public error(message: string): void { 17 | this.log("error", message); 18 | } 19 | 20 | private log(level: keyof Console, message: string) { 21 | if (console) { 22 | const msg = `Exceptionless:${new Date().toISOString()} [${level}] ${message}`; 23 | const logFn = console[level] as (msg: string) => void; 24 | if (logFn) { 25 | logFn(msg); 26 | } else if (console["log"]) { 27 | console["log"](msg); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/logging/ILog.ts: -------------------------------------------------------------------------------- 1 | export interface ILog { 2 | trace(message: string): void; 3 | info(message: string): void; 4 | warn(message: string): void; 5 | error(message: string): void; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/logging/NullLog.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { ILog } from "./ILog.js"; 3 | 4 | export class NullLog implements ILog { 5 | public trace(_: string): void { 6 | /* empty */ 7 | } 8 | public info(_: string): void { 9 | /* empty */ 10 | } 11 | public warn(_: string): void { 12 | /* empty */ 13 | } 14 | public error(_: string): void { 15 | /* empty */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/models/Event.ts: -------------------------------------------------------------------------------- 1 | import { ErrorInfo, SimpleError } from "./data/ErrorInfo.js"; 2 | import { EnvironmentInfo } from "./data/EnvironmentInfo.js"; 3 | import { RequestInfo } from "./data/RequestInfo.js"; 4 | import { UserInfo } from "./data/UserInfo.js"; 5 | import { UserDescription } from "./data/UserDescription.js"; 6 | import { ManualStackingInfo } from "./data/ManualStackingInfo.js"; 7 | 8 | export type EventType = "error" | "usage" | "log" | "404" | "session" | string; 9 | 10 | export interface Event { 11 | /** The event type (ie. error, log message, feature usage). */ 12 | type?: EventType; 13 | /** The event source (ie. machine name, log name, feature name). */ 14 | source?: string; 15 | /** The date that the event occurred on. */ 16 | date?: Date; 17 | /** A list of tags used to categorize this event. */ 18 | tags?: string[]; 19 | /** The event message. */ 20 | message?: string; 21 | /** The geo coordinates where the event happened. */ 22 | geo?: string; 23 | /** The value of the event if any. */ 24 | value?: number; 25 | /** The number of duplicated events. */ 26 | count?: number; 27 | /** An optional identifier to be used for referencing this event instance at a later time. */ 28 | reference_id?: string; 29 | /** Optional data entries that contain additional information about this event. */ 30 | data?: IEventData; 31 | } 32 | 33 | export enum KnownEventDataKeys { 34 | Error = "@error", 35 | SimpleError = "@simple_error", 36 | RequestInfo = "@request", 37 | TraceLog = "@trace", 38 | EnvironmentInfo = "@environment", 39 | UserInfo = "@user", 40 | UserDescription = "@user_description", 41 | Version = "@version", 42 | Level = "@level", 43 | SubmissionMethod = "@submission_method", 44 | ManualStackingInfo = "@stack" 45 | } 46 | 47 | export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal" | string; 48 | 49 | export interface IEventData extends Record { 50 | "@error"?: ErrorInfo; 51 | "@simple_error"?: SimpleError; 52 | "@request"?: RequestInfo; 53 | "@environment"?: EnvironmentInfo; 54 | "@user"?: UserInfo; 55 | "@user_description"?: UserDescription; 56 | "@version"?: string; 57 | "@level"?: LogLevel; 58 | "@submission_method"?: string; 59 | "@stack"?: ManualStackingInfo; 60 | } 61 | -------------------------------------------------------------------------------- /packages/core/src/models/EventContext.ts: -------------------------------------------------------------------------------- 1 | const enum KnownContextKeys { 2 | Exception = "@@_Exception", 3 | IsUnhandledError = "@@_IsUnhandledError", 4 | SubmissionMethod = "@@_SubmissionMethod" 5 | } 6 | 7 | export class EventContext implements Record { 8 | [x: string]: unknown; 9 | 10 | public getException(): Error | null { 11 | return (this[KnownContextKeys.Exception] as Error) || null; 12 | } 13 | 14 | public setException(exception: Error): void { 15 | if (exception) { 16 | this[KnownContextKeys.Exception] = exception; 17 | } 18 | } 19 | 20 | public get hasException(): boolean { 21 | return !!this[KnownContextKeys.Exception]; 22 | } 23 | 24 | public markAsUnhandledError(): void { 25 | this[KnownContextKeys.IsUnhandledError] = true; 26 | } 27 | 28 | public get isUnhandledError(): boolean { 29 | return !!this[KnownContextKeys.IsUnhandledError]; 30 | } 31 | 32 | public getSubmissionMethod(): string | null { 33 | return (this[KnownContextKeys.SubmissionMethod] as string) || null; 34 | } 35 | 36 | public setSubmissionMethod(method: string): void { 37 | if (method) { 38 | this[KnownContextKeys.SubmissionMethod] = method; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/models/data/EnvironmentInfo.ts: -------------------------------------------------------------------------------- 1 | export interface EnvironmentInfo { 2 | processor_count?: number; 3 | total_physical_memory?: number; 4 | available_physical_memory?: number; 5 | command_line?: string; 6 | process_name?: string; 7 | process_id?: string; 8 | process_memory_size?: number; 9 | thread_id?: string; 10 | architecture?: string; 11 | o_s_name?: string; 12 | o_s_version?: string; 13 | ip_address?: string; 14 | machine_name?: string; 15 | install_id?: string; 16 | runtime_version?: string; 17 | data?: Record; 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/models/data/ErrorInfo.ts: -------------------------------------------------------------------------------- 1 | import { ModuleInfo } from "./ModuleInfo.js"; 2 | 3 | export interface SimpleError { 4 | message?: string; 5 | type?: string; 6 | stack_trace?: string; 7 | data?: Record; 8 | inner?: SimpleError; 9 | } 10 | 11 | export interface InnerErrorInfo { 12 | message?: string; 13 | type?: string; 14 | code?: string; 15 | data?: Record; 16 | inner?: InnerErrorInfo; 17 | stack_trace?: StackFrameInfo[]; 18 | target_method?: MethodInfo; 19 | } 20 | 21 | export interface ErrorInfo extends InnerErrorInfo { 22 | modules?: ModuleInfo[]; 23 | } 24 | 25 | export interface MethodInfo { 26 | data?: Record; 27 | generic_arguments?: string[]; 28 | parameters?: ParameterInfo[]; 29 | is_signature_target?: boolean; 30 | declaring_namespace?: string; 31 | declaring_type?: string; 32 | name?: string; 33 | module_id?: number; 34 | } 35 | 36 | export interface ParameterInfo { 37 | data?: Record; 38 | generic_arguments?: string[]; 39 | name?: string; 40 | type?: string; 41 | type_namespace?: string; 42 | } 43 | 44 | export interface StackFrameInfo extends MethodInfo { 45 | file_name?: string; 46 | line_number?: number; 47 | column?: number; 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/models/data/ManualStackingInfo.ts: -------------------------------------------------------------------------------- 1 | export interface ManualStackingInfo { 2 | title?: string; 3 | signature_data?: Record; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/models/data/ModuleInfo.ts: -------------------------------------------------------------------------------- 1 | export interface ModuleInfo { 2 | data?: Record; 3 | module_id?: number; 4 | name?: string; 5 | version?: string; 6 | is_entry?: boolean; 7 | created_date?: Date; 8 | modified_date?: Date; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/models/data/RequestInfo.ts: -------------------------------------------------------------------------------- 1 | export interface RequestInfo { 2 | user_agent?: string; 3 | http_method?: string; 4 | is_secure?: boolean; 5 | host?: string; 6 | port?: number; 7 | path?: string; 8 | referrer?: string; 9 | client_ip_address?: string; 10 | headers?: Record; 11 | cookies?: Record; 12 | post_data?: Record; 13 | query_string?: Record; 14 | data?: Record; 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/models/data/UserDescription.ts: -------------------------------------------------------------------------------- 1 | export interface UserDescription { 2 | email_address?: string; 3 | description: string; 4 | data?: Record; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/models/data/UserInfo.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfo { 2 | identity?: string; 3 | name?: string; 4 | data?: Record; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/plugins/EventPluginContext.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient } from "../ExceptionlessClient.js"; 2 | import { ILog } from "../logging/ILog.js"; 3 | import { Event } from "../models/Event.js"; 4 | import { EventContext } from "../models/EventContext.js"; 5 | 6 | export class EventPluginContext { 7 | public cancelled: boolean = false; 8 | 9 | constructor( 10 | public client: ExceptionlessClient, 11 | public event: Event, 12 | public eventContext: EventContext 13 | ) { 14 | if (!this.eventContext) this.eventContext = new EventContext(); 15 | } 16 | 17 | public get log(): ILog { 18 | return this.client.config.services.log; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/plugins/EventPluginManager.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "../configuration/Configuration.js"; 2 | import { ConfigurationDefaultsPlugin } from "./default/ConfigurationDefaultsPlugin.js"; 3 | import { DuplicateCheckerPlugin } from "./default/DuplicateCheckerPlugin.js"; 4 | import { EventPluginContext } from "./EventPluginContext.js"; 5 | import { EventExclusionPlugin } from "./default/EventExclusionPlugin.js"; 6 | import { PluginContext } from "./PluginContext.js"; 7 | import { ReferenceIdPlugin } from "./default/ReferenceIdPlugin.js"; 8 | import { SimpleErrorPlugin } from "./default/SimpleErrorPlugin.js"; 9 | import { SubmissionMethodPlugin } from "./default/SubmissionMethodPlugin.js"; 10 | 11 | export class EventPluginManager { 12 | public static async startup(context: PluginContext): Promise { 13 | for (const plugin of context.client.config.plugins) { 14 | if (!plugin.startup) { 15 | continue; 16 | } 17 | 18 | try { 19 | await plugin.startup(context); 20 | } catch (ex) { 21 | context.log.error(`Error running plugin startup"${plugin.name}": ${ex instanceof Error ? ex.message : ex + ""}`); 22 | } 23 | } 24 | } 25 | 26 | public static async suspend(context: PluginContext): Promise { 27 | for (const plugin of context.client.config.plugins) { 28 | if (!plugin.suspend) { 29 | continue; 30 | } 31 | 32 | try { 33 | await plugin.suspend(context); 34 | } catch (ex) { 35 | context.log.error(`Error running plugin suspend"${plugin.name}": ${ex instanceof Error ? ex.message : ex + ""}`); 36 | } 37 | } 38 | } 39 | 40 | public static async run(context: EventPluginContext): Promise { 41 | for (const plugin of context.client.config.plugins) { 42 | if (context.cancelled) { 43 | break; 44 | } 45 | 46 | if (!plugin.run) { 47 | continue; 48 | } 49 | 50 | try { 51 | await plugin.run(context); 52 | } catch (ex) { 53 | context.cancelled = true; 54 | context.log.error(`Error running plugin "${plugin.name}": ${ex instanceof Error ? ex.message : ex + ""}. Discarding Event.`); 55 | } 56 | } 57 | } 58 | 59 | public static addDefaultPlugins(config: Configuration): void { 60 | config.addPlugin(new ConfigurationDefaultsPlugin()); 61 | config.addPlugin(new SimpleErrorPlugin()); 62 | config.addPlugin(new ReferenceIdPlugin()); 63 | config.addPlugin(new DuplicateCheckerPlugin()); 64 | config.addPlugin(new EventExclusionPlugin()); 65 | config.addPlugin(new SubmissionMethodPlugin()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/core/src/plugins/IEventPlugin.ts: -------------------------------------------------------------------------------- 1 | import { EventPluginContext } from "./EventPluginContext.js"; 2 | import { PluginContext } from "./PluginContext.js"; 3 | 4 | export interface IEventPlugin { 5 | priority?: number; 6 | name?: string; 7 | startup?(context: PluginContext): Promise; 8 | suspend?(context: PluginContext): Promise; 9 | run?(context: EventPluginContext): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/plugins/PluginContext.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient } from "../ExceptionlessClient.js"; 2 | import { ILog } from "../logging/ILog.js"; 3 | 4 | export class PluginContext { 5 | constructor(public client: ExceptionlessClient) {} 6 | 7 | public get log(): ILog { 8 | return this.client.config.services.log; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/ConfigurationDefaultsPlugin.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty, stringify } from "../../Utils.js"; 2 | import { EventPluginContext } from "../EventPluginContext.js"; 3 | import { IEventPlugin } from "../IEventPlugin.js"; 4 | 5 | export class ConfigurationDefaultsPlugin implements IEventPlugin { 6 | public priority = 10; 7 | public name = "ConfigurationDefaultsPlugin"; 8 | 9 | public run(context: EventPluginContext): Promise { 10 | const { dataExclusions, defaultData, defaultTags } = context.client.config; 11 | const ev = context.event; 12 | 13 | if (defaultTags) { 14 | ev.tags = [...(ev.tags || []), ...defaultTags]; 15 | } 16 | 17 | if (defaultData) { 18 | if (!ev.data) { 19 | ev.data = {}; 20 | } 21 | 22 | for (const key in defaultData) { 23 | if (ev.data[key] !== undefined || isEmpty(defaultData[key])) { 24 | continue; 25 | } 26 | 27 | const data = stringify(defaultData[key], dataExclusions); 28 | if (!isEmpty(data)) { 29 | ev.data[key] = JSON.parse(data); 30 | } 31 | } 32 | } 33 | 34 | return Promise.resolve(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/DuplicateCheckerPlugin.ts: -------------------------------------------------------------------------------- 1 | import { InnerErrorInfo } from "../../models/data/ErrorInfo.js"; 2 | import { KnownEventDataKeys } from "../../models/Event.js"; 3 | import { allowProcessToExitWithoutWaitingForTimerOrInterval, getHashCode } from "../../Utils.js"; 4 | import { EventPluginContext } from "../EventPluginContext.js"; 5 | import { IEventPlugin } from "../IEventPlugin.js"; 6 | 7 | export class DuplicateCheckerPlugin implements IEventPlugin { 8 | public priority = 1010; 9 | public name = "DuplicateCheckerPlugin"; 10 | 11 | private _mergedEvents: MergedEvent[] = []; 12 | private _processedHashCodes: TimestampedHash[] = []; 13 | private _getCurrentTime: () => number; 14 | private _intervalId: ReturnType | undefined; 15 | private _interval: number; 16 | 17 | constructor(getCurrentTime: () => number = () => Date.now(), interval: number = 30000) { 18 | this._getCurrentTime = getCurrentTime; 19 | this._interval = interval; 20 | } 21 | 22 | public startup(): Promise { 23 | clearInterval(this._intervalId); 24 | this._intervalId = setInterval(() => void this.submitEvents(), this._interval); 25 | allowProcessToExitWithoutWaitingForTimerOrInterval(this._intervalId); 26 | return Promise.resolve(); 27 | } 28 | 29 | public async suspend(): Promise { 30 | clearInterval(this._intervalId); 31 | this._intervalId = undefined; 32 | await this.submitEvents(); 33 | } 34 | 35 | public run(context: EventPluginContext): Promise { 36 | function calculateHashCode(error: InnerErrorInfo | undefined): number { 37 | let hash = 0; 38 | while (error) { 39 | if (error.message && error.message.length) { 40 | hash += (hash * 397) ^ getHashCode(error.message); 41 | } 42 | if (error.stack_trace && error.stack_trace.length) { 43 | hash += (hash * 397) ^ getHashCode(JSON.stringify(error.stack_trace)); 44 | } 45 | error = error.inner; 46 | } 47 | 48 | return hash; 49 | } 50 | 51 | const error = context.event.data?.[KnownEventDataKeys.Error]; 52 | const hashCode = calculateHashCode(error); 53 | if (hashCode) { 54 | const count = context.event.count || 1; 55 | const now = this._getCurrentTime(); 56 | 57 | const merged = this._mergedEvents.filter((s) => s.hashCode === hashCode)[0]; 58 | if (merged) { 59 | merged.incrementCount(count); 60 | merged.updateDate(context.event.date); 61 | context.log.info("Ignoring duplicate event with hash: " + hashCode); 62 | context.cancelled = true; 63 | } 64 | 65 | if (!context.cancelled && this._processedHashCodes.some((h) => h.hash === hashCode && h.timestamp >= now - this._interval)) { 66 | context.log.trace("Adding event with hash: " + hashCode); 67 | this._mergedEvents.push(new MergedEvent(hashCode, context, count)); 68 | context.cancelled = true; 69 | } 70 | 71 | if (!context.cancelled) { 72 | context.log.trace(`Enqueueing event with hash: ${hashCode} to cache`); 73 | this._processedHashCodes.push({ hash: hashCode, timestamp: now }); 74 | 75 | // Only keep the last 50 recent errors. 76 | while (this._processedHashCodes.length > 50) { 77 | this._processedHashCodes.shift(); 78 | } 79 | } 80 | } 81 | 82 | return Promise.resolve(); 83 | } 84 | 85 | private async submitEvents(): Promise { 86 | while (this._mergedEvents.length > 0) { 87 | await this._mergedEvents.shift()?.resubmit(); 88 | } 89 | } 90 | } 91 | 92 | interface TimestampedHash { 93 | hash: number; 94 | timestamp: number; 95 | } 96 | 97 | class MergedEvent { 98 | public hashCode: number; 99 | private _count: number; 100 | private _context: EventPluginContext; 101 | 102 | constructor(hashCode: number, context: EventPluginContext, count: number) { 103 | this.hashCode = hashCode; 104 | this._context = context; 105 | this._count = count; 106 | } 107 | 108 | public incrementCount(count: number): void { 109 | this._count += count; 110 | } 111 | 112 | public async resubmit(): Promise { 113 | this._context.event.count = this._count; 114 | await this._context.client.config.services.queue.enqueue(this._context.event); 115 | } 116 | 117 | public updateDate(date?: Date): void { 118 | const ev = this._context.event; 119 | if (date && ev.date && date > ev.date) { 120 | ev.date = date; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/EventExclusionPlugin.ts: -------------------------------------------------------------------------------- 1 | import { KnownEventDataKeys, LogLevel } from "../../models/Event.js"; 2 | import { isMatch, startsWith, toBoolean } from "../../Utils.js"; 3 | import { EventPluginContext } from "../EventPluginContext.js"; 4 | import { IEventPlugin } from "../IEventPlugin.js"; 5 | 6 | export class EventExclusionPlugin implements IEventPlugin { 7 | public priority = 45; 8 | public name = "EventExclusionPlugin"; 9 | 10 | public run(context: EventPluginContext): Promise { 11 | const ev = context.event; 12 | const log = context.log; 13 | const settings = context.client.config.settings; 14 | 15 | if (ev.type === "log") { 16 | const minLogLevel = this.getMinLogLevel(settings, ev.source); 17 | const logLevel = this.getLogLevel(ev.data && ev.data[KnownEventDataKeys.Level]); 18 | 19 | if (logLevel !== -1 && (logLevel === 6 || logLevel < minLogLevel)) { 20 | log.info("Cancelling log event due to minimum log level."); 21 | context.cancelled = true; 22 | } 23 | } else if (ev.type === "error") { 24 | let error = ev.data && ev.data[KnownEventDataKeys.Error]; 25 | while (!context.cancelled && error) { 26 | if (this.getTypeAndSourceSetting(settings, ev.type, error.type, true) === false) { 27 | log.info(`Cancelling error from excluded exception type: ${error.type}`); 28 | context.cancelled = true; 29 | } 30 | 31 | error = error.inner; 32 | } 33 | } else if (this.getTypeAndSourceSetting(settings, ev.type, ev.source, true) === false) { 34 | log.info(`Cancelling event from excluded type: ${ev.type} and source: ${ev.source}`); 35 | context.cancelled = true; 36 | } 37 | 38 | return Promise.resolve(); 39 | } 40 | 41 | public getLogLevel(level: LogLevel | undefined): number { 42 | switch ((level || "").toLowerCase().trim()) { 43 | case "trace": 44 | case "true": 45 | case "1": 46 | case "yes": 47 | return 0; 48 | case "debug": 49 | return 1; 50 | case "info": 51 | return 2; 52 | case "warn": 53 | return 3; 54 | case "error": 55 | return 4; 56 | case "fatal": 57 | return 5; 58 | case "off": 59 | case "false": 60 | case "0": 61 | case "no": 62 | return 6; 63 | default: 64 | return -1; 65 | } 66 | } 67 | 68 | public getMinLogLevel(configSettings: Record, source: string | undefined): number { 69 | return this.getLogLevel(this.getTypeAndSourceSetting(configSettings, "log", source, "other") + ""); 70 | } 71 | 72 | private getTypeAndSourceSetting( 73 | configSettings: Record = {}, 74 | type: string | undefined, 75 | source: string | undefined, 76 | defaultValue: string | boolean 77 | ): string | boolean { 78 | if (!type) { 79 | return defaultValue; 80 | } 81 | 82 | if (!source) { 83 | source = ""; 84 | } 85 | 86 | const isLog: boolean = type === "log"; 87 | const sourcePrefix = `@@${type}:`; 88 | 89 | const value: string = configSettings[sourcePrefix + source]; 90 | if (value) { 91 | return isLog ? value : toBoolean(value); 92 | } 93 | 94 | // sort object keys longest first, then alphabetically. 95 | const sortedKeys = Object.keys(configSettings).sort((a, b) => b.length - a.length || a.localeCompare(b)); 96 | for (const key of sortedKeys) { 97 | if (!startsWith(key.toLowerCase(), sourcePrefix)) { 98 | continue; 99 | } 100 | 101 | // check for wildcard match 102 | const cleanKey: string = key.substring(sourcePrefix.length); 103 | if (isMatch(source, [cleanKey])) { 104 | return isLog ? configSettings[key] : toBoolean(configSettings[key]); 105 | } 106 | } 107 | 108 | return defaultValue; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/HeartbeatPlugin.ts: -------------------------------------------------------------------------------- 1 | import { KnownEventDataKeys } from "../../models/Event.js"; 2 | import { allowProcessToExitWithoutWaitingForTimerOrInterval } from "../../Utils.js"; 3 | import { EventPluginContext } from "../EventPluginContext.js"; 4 | import { IEventPlugin } from "../IEventPlugin.js"; 5 | 6 | export class HeartbeatPlugin implements IEventPlugin { 7 | public priority = 100; 8 | public name = "HeartbeatPlugin"; 9 | 10 | private _interval: number; 11 | private _intervalId: ReturnType | undefined; 12 | 13 | constructor(heartbeatInterval: number = 60000) { 14 | this._interval = heartbeatInterval >= 30000 ? heartbeatInterval : 60000; 15 | } 16 | 17 | public startup(): Promise { 18 | clearInterval(this._intervalId); 19 | this._intervalId = undefined; 20 | // TODO: Do we want to send a heartbeat for the last user? 21 | return Promise.resolve(); 22 | } 23 | 24 | public suspend(): Promise { 25 | clearInterval(this._intervalId); 26 | this._intervalId = undefined; 27 | return Promise.resolve(); 28 | } 29 | 30 | public run(context: EventPluginContext): Promise { 31 | if (this._interval <= 0) { 32 | return Promise.resolve(); 33 | } 34 | 35 | clearInterval(this._intervalId); 36 | this._intervalId = undefined; 37 | 38 | const { config } = context.client; 39 | if (!config.currentSessionIdentifier) { 40 | const user = context.event.data?.[KnownEventDataKeys.UserInfo]; 41 | if (!user?.identity) { 42 | return Promise.resolve(); 43 | } 44 | 45 | config.currentSessionIdentifier = user.identity; 46 | } 47 | 48 | if (config.currentSessionIdentifier) { 49 | this._intervalId = setInterval(() => void context.client.submitSessionHeartbeat(config.currentSessionIdentifier), this._interval); 50 | 51 | allowProcessToExitWithoutWaitingForTimerOrInterval(this._intervalId); 52 | } 53 | 54 | return Promise.resolve(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/ReferenceIdPlugin.ts: -------------------------------------------------------------------------------- 1 | import { guid } from "../../Utils.js"; 2 | import { EventPluginContext } from "../EventPluginContext.js"; 3 | import { IEventPlugin } from "../IEventPlugin.js"; 4 | 5 | export class ReferenceIdPlugin implements IEventPlugin { 6 | public priority = 20; 7 | public name = "ReferenceIdPlugin"; 8 | 9 | public run(context: EventPluginContext): Promise { 10 | if (!context.event.reference_id && context.event.type === "error") { 11 | // PERF: Optimize identifier creation. 12 | context.event.reference_id = guid().replaceAll("-", "").substring(0, 10); 13 | } 14 | 15 | return Promise.resolve(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/SessionIdManagementPlugin.ts: -------------------------------------------------------------------------------- 1 | import { guid } from "../../Utils.js"; 2 | import { EventPluginContext } from "../EventPluginContext.js"; 3 | import { IEventPlugin } from "../IEventPlugin.js"; 4 | 5 | export class SessionIdManagementPlugin implements IEventPlugin { 6 | public priority = 25; 7 | public name = "SessionIdManagementPlugin"; 8 | 9 | public run(context: EventPluginContext): Promise { 10 | const ev = context.event; 11 | const isSessionStart: boolean = ev.type === "session"; 12 | const { config } = context.client; 13 | if (isSessionStart || !config.currentSessionIdentifier) { 14 | config.currentSessionIdentifier = guid().replaceAll("-", ""); 15 | } 16 | 17 | if (isSessionStart) { 18 | ev.reference_id = config.currentSessionIdentifier; 19 | } else { 20 | if (!ev.data) { 21 | ev.data = {}; 22 | } 23 | 24 | ev.data["@ref:session"] = config.currentSessionIdentifier; 25 | } 26 | 27 | return Promise.resolve(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/SimpleErrorPlugin.ts: -------------------------------------------------------------------------------- 1 | import { EventPluginContext } from "../EventPluginContext.js"; 2 | import { IEventPlugin } from "../IEventPlugin.js"; 3 | import { isEmpty, stringify } from "../../Utils.js"; 4 | import { SimpleError } from "../../models/data/ErrorInfo.js"; 5 | import { KnownEventDataKeys } from "../../models/Event.js"; 6 | 7 | export const IgnoredErrorProperties: string[] = [ 8 | "arguments", 9 | "column", 10 | "columnNumber", 11 | "description", 12 | "fileName", 13 | "message", 14 | "name", 15 | "number", 16 | "line", 17 | "lineNumber", 18 | "opera#sourceloc", 19 | "sourceId", 20 | "sourceURL", 21 | "stack", 22 | "stackArray", 23 | "stacktrace" 24 | ]; 25 | 26 | export class SimpleErrorPlugin implements IEventPlugin { 27 | public priority = 30; 28 | public name = "SimpleErrorPlugin"; 29 | 30 | public async run(context: EventPluginContext): Promise { 31 | const exception = context.eventContext.getException(); 32 | if (exception) { 33 | if (!context.event.type) { 34 | context.event.type = "error"; 35 | } 36 | 37 | if (context.event.data && !context.event.data[KnownEventDataKeys.SimpleError]) { 38 | const error = { 39 | type: exception.name || "Error", 40 | message: exception.message, 41 | stack_trace: exception.stack, 42 | data: {} 43 | }; 44 | 45 | const exclusions = context.client.config.dataExclusions.concat(IgnoredErrorProperties); 46 | const additionalData = stringify(exception, exclusions); 47 | if (!isEmpty(additionalData)) { 48 | error.data!["@ext"] = JSON.parse(additionalData); 49 | } 50 | 51 | context.event.data[KnownEventDataKeys.SimpleError] = error; 52 | } 53 | } 54 | 55 | return Promise.resolve(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/plugins/default/SubmissionMethodPlugin.ts: -------------------------------------------------------------------------------- 1 | import { KnownEventDataKeys } from "../../models/Event.js"; 2 | import { EventPluginContext } from "../EventPluginContext.js"; 3 | import { IEventPlugin } from "../IEventPlugin.js"; 4 | 5 | export class SubmissionMethodPlugin implements IEventPlugin { 6 | public priority = 100; 7 | public name = "SubmissionMethodPlugin"; 8 | 9 | public run(context: EventPluginContext): Promise { 10 | const submissionMethod = context.eventContext.getSubmissionMethod(); 11 | if (submissionMethod && context.event.data) { 12 | context.event.data[KnownEventDataKeys.SubmissionMethod] = submissionMethod; 13 | } 14 | 15 | return Promise.resolve(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/queue/IEventQueue.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "../models/Event.js"; 2 | import { Response } from "../submission/Response.js"; 3 | 4 | export interface IEventQueue { 5 | /** Enqueue an event */ 6 | enqueue(event: Event): Promise; 7 | /** Processes all events in the queue */ 8 | process(): Promise; 9 | /** Starts queue timers */ 10 | startup(): Promise; 11 | /** Suspends queue timers */ 12 | suspend(): Promise; 13 | /** Suspends processing of events for a specific duration */ 14 | suspendProcessing(durationInMinutes?: number, discardFutureQueuedItems?: boolean, clearQueue?: boolean): Promise; 15 | // TODO: See if this makes sense. 16 | onEventsPosted(handler: (events: Event[], response: Response) => Promise): void; 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/storage/IStorage.ts: -------------------------------------------------------------------------------- 1 | export interface IStorage { 2 | length(): Promise; 3 | clear(): Promise; 4 | getItem(key: string): Promise; 5 | key(index: number): Promise; 6 | keys(): Promise; 7 | removeItem(key: string): Promise; 8 | setItem(key: string, value: string): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/storage/InMemoryStorage.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "./IStorage.js"; 2 | 3 | export class InMemoryStorage implements IStorage { 4 | private items = new Map(); 5 | 6 | public length(): Promise { 7 | return Promise.resolve(this.items.size); 8 | } 9 | 10 | public clear(): Promise { 11 | this.items.clear(); 12 | return Promise.resolve(); 13 | } 14 | 15 | public getItem(key: string): Promise { 16 | const value = this.items.get(key); 17 | return Promise.resolve(value ? value : null); 18 | } 19 | 20 | public async key(index: number): Promise { 21 | if (index < 0) return Promise.resolve(null); 22 | 23 | const keys = await this.keys(); 24 | 25 | if (index > keys.length) return Promise.resolve(null); 26 | 27 | const key = keys[index]; 28 | return Promise.resolve(key ? key : null); 29 | } 30 | 31 | public keys(): Promise { 32 | return Promise.resolve(Array.from(this.items.keys())); 33 | } 34 | 35 | public removeItem(key: string): Promise { 36 | this.items.delete(key); 37 | return Promise.resolve(); 38 | } 39 | 40 | public setItem(key: string, value: string): Promise { 41 | this.items.set(key, value); 42 | return Promise.resolve(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/storage/LocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "./IStorage.js"; 2 | 3 | export class LocalStorage implements IStorage { 4 | constructor( 5 | private prefix: string = "exceptionless-", 6 | private storage: Storage = globalThis.localStorage 7 | ) {} 8 | 9 | public length(): Promise { 10 | return Promise.resolve(this.getKeys().length); 11 | } 12 | 13 | public clear(): Promise { 14 | for (const key of this.getKeys()) { 15 | this.storage.removeItem(this.getKey(key)); 16 | } 17 | 18 | return Promise.resolve(); 19 | } 20 | 21 | public getItem(key: string): Promise { 22 | return Promise.resolve(this.storage.getItem(this.getKey(key))); 23 | } 24 | 25 | public key(index: number): Promise { 26 | const keys = this.getKeys(); 27 | return Promise.resolve(index < keys.length ? keys[index] : null); 28 | } 29 | 30 | public keys(): Promise { 31 | return Promise.resolve(this.getKeys()); 32 | } 33 | 34 | public removeItem(key: string): Promise { 35 | this.storage.removeItem(this.getKey(key)); 36 | return Promise.resolve(); 37 | } 38 | 39 | public setItem(key: string, value: string): Promise { 40 | this.storage.setItem(this.getKey(key), value); 41 | return Promise.resolve(); 42 | } 43 | 44 | private getKeys(): string[] { 45 | return Object.keys(this.storage) 46 | .filter((key) => key.startsWith(this.prefix)) 47 | .map((key) => key?.substr(this.prefix.length)); 48 | } 49 | 50 | private getKey(key: string): string { 51 | return this.prefix + key; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/src/submission/DefaultSubmissionClient.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "../configuration/Configuration.js"; 2 | import { ServerSettings, SettingsManager } from "../configuration/SettingsManager.js"; 3 | import { Event } from "../models/Event.js"; 4 | import { UserDescription } from "../models/data/UserDescription.js"; 5 | import { ISubmissionClient } from "./ISubmissionClient.js"; 6 | import { Response } from "./Response.js"; 7 | 8 | export class DefaultSubmissionClient implements ISubmissionClient { 9 | protected readonly RateLimitRemainingHeader: string = "x-ratelimit-remaining"; 10 | protected readonly ConfigurationVersionHeader: string = "x-exceptionless-configversion"; 11 | 12 | public constructor( 13 | protected config: Configuration, 14 | private fetch = globalThis.fetch?.bind(globalThis) 15 | ) {} 16 | 17 | public getSettings(version: number): Promise> { 18 | const url = `${this.config.serverUrl}/api/v2/projects/config?v=${version}`; 19 | return this.apiFetch(url, { 20 | method: "GET" 21 | }); 22 | } 23 | 24 | public async submitEvents(events: Event[]): Promise { 25 | const url = `${this.config.serverUrl}/api/v2/events`; 26 | const response = await this.apiFetch(url, { 27 | method: "POST", 28 | body: JSON.stringify(events) 29 | }); 30 | 31 | await this.updateSettingsVersion(response.settingsVersion); 32 | return response; 33 | } 34 | 35 | public async submitUserDescription(referenceId: string, description: UserDescription): Promise { 36 | const url = `${this.config.serverUrl}/api/v2/events/by-ref/${encodeURIComponent(referenceId)}/user-description`; 37 | 38 | const response = await this.apiFetch(url, { 39 | method: "POST", 40 | body: JSON.stringify(description) 41 | }); 42 | 43 | await this.updateSettingsVersion(response.settingsVersion); 44 | 45 | return response; 46 | } 47 | 48 | public async submitHeartbeat(sessionIdOrUserId: string, closeSession: boolean): Promise> { 49 | const url = `${this.config.heartbeatServerUrl}/api/v2/events/session/heartbeat?id=${sessionIdOrUserId}&close=${closeSession + ""}`; 50 | return await this.apiFetch(url, { 51 | method: "GET" 52 | }); 53 | } 54 | 55 | protected async updateSettingsVersion(serverSettingsVersion: number): Promise { 56 | if (isNaN(serverSettingsVersion)) { 57 | this.config.services.log.error("No config version header was returned."); 58 | } else if (serverSettingsVersion > this.config.settingsVersion) { 59 | await SettingsManager.updateSettings(this.config); 60 | } 61 | } 62 | 63 | protected async apiFetch(url: string, options: FetchOptions): Promise> { 64 | // TODO: Figure out how to set a 10000 timeout. 65 | const requestOptions: RequestInit = { 66 | method: options.method, 67 | headers: { 68 | Accept: "application/json", 69 | Authorization: `Bearer ${this.config.apiKey}`, 70 | "User-Agent": this.config.userAgent 71 | }, 72 | body: options.body ?? null 73 | }; 74 | 75 | // TODO: Can we properly calculate content size? 76 | if (options.method === "POST" && this.isHeaders(requestOptions.headers)) { 77 | requestOptions.headers["Content-Type"] = "application/json"; 78 | } 79 | 80 | const response = await this.fetch(url, requestOptions); 81 | const rateLimitRemaining: number = parseInt(response.headers.get(this.RateLimitRemainingHeader) || "", 10); 82 | const settingsVersion: number = parseInt(response.headers.get(this.ConfigurationVersionHeader) || "", 10); 83 | 84 | const responseText = await response.text(); 85 | const data = responseText && responseText.length > 0 ? (JSON.parse(responseText) as T) : null; 86 | 87 | return new Response(response.status, response.statusText, rateLimitRemaining, settingsVersion, data); 88 | } 89 | 90 | private isHeaders(headers: HeadersInit | undefined): headers is Record { 91 | return (headers as Record) !== undefined; 92 | } 93 | } 94 | 95 | export interface FetchOptions { 96 | method: "GET" | "POST"; 97 | body?: string; 98 | } 99 | -------------------------------------------------------------------------------- /packages/core/src/submission/ISubmissionClient.ts: -------------------------------------------------------------------------------- 1 | import { ServerSettings } from "../configuration/SettingsManager.js"; 2 | import { Event } from "../models/Event.js"; 3 | import { UserDescription } from "../models/data/UserDescription.js"; 4 | import { Response } from "./Response.js"; 5 | 6 | export interface ISubmissionClient { 7 | getSettings(version: number): Promise>; 8 | submitEvents(events: Event[]): Promise; 9 | submitUserDescription(referenceId: string, description: UserDescription): Promise; 10 | submitHeartbeat(sessionIdOrUserId: string, closeSession: boolean): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/submission/Response.ts: -------------------------------------------------------------------------------- 1 | export class Response { 2 | constructor( 3 | public status: number, 4 | public message: string, 5 | public rateLimitRemaining: number, 6 | public settingsVersion: number, 7 | public data: T 8 | ) {} 9 | 10 | public get success(): boolean { 11 | return this.status >= 200 && this.status <= 299; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/test/ExceptionlessClient.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { ExceptionlessClient } from "../src/ExceptionlessClient.js"; 5 | import { KnownEventDataKeys } from "../src/models/Event.js"; 6 | import { ReferenceIdPlugin } from "../src/plugins/default/ReferenceIdPlugin.js"; 7 | 8 | describe("ExceptionlessClient", () => { 9 | test("should use event reference ids", async () => { 10 | const error = createException(); 11 | 12 | const client = new ExceptionlessClient(); 13 | client.config.apiKey = "UNIT_TEST_API_KEY"; 14 | 15 | expect(client.config.plugins).not.toBeNull(); 16 | while (client.config.plugins.length > 0) { 17 | client.config.removePlugin(client.config.plugins[0]); 18 | } 19 | 20 | const { lastReferenceIdManager } = client.config.services; 21 | expect(lastReferenceIdManager.getLast()).toBeNull(); 22 | 23 | let context = await client.submitException(error); 24 | expect(context.event.reference_id).toBeUndefined(); 25 | expect(lastReferenceIdManager.getLast()).toBeNull(); 26 | 27 | client.config.addPlugin(new ReferenceIdPlugin()); 28 | expect(client.config.plugins.length).toBe(1); 29 | 30 | context = await client.submitException(error); 31 | expect(context.event.reference_id).not.toBeUndefined(); 32 | const lastReference = lastReferenceIdManager.getLast(); 33 | expect(context.event.reference_id).toBe(lastReference); 34 | 35 | context = await client.submitException(error); 36 | expect(context.event.reference_id).not.toBeUndefined(); 37 | expect(context.event.reference_id).not.toBe(lastReference); 38 | expect(context.event.reference_id).toBe(lastReferenceIdManager.getLast()); 39 | }); 40 | 41 | test("should accept undefined source", () => { 42 | const client = new ExceptionlessClient(); 43 | client.config.apiKey = "UNIT_TEST_API_KEY"; 44 | 45 | const builder = client.createLog(undefined, "Unit Test message", "trace"); 46 | 47 | expect(builder.target.source).toBeUndefined(); 48 | expect(builder.target.message).toBe("Unit Test message"); 49 | expect(builder.target.data?.[KnownEventDataKeys.Level]).toBe("trace"); 50 | }); 51 | 52 | test("should accept source and message", () => { 53 | const client = new ExceptionlessClient(); 54 | client.config.apiKey = "UNIT_TEST_API_KEY"; 55 | 56 | const builder = client.createLog("ExceptionlessClient", "Unit Test message"); 57 | 58 | expect(builder.target.source).toBe("ExceptionlessClient"); 59 | expect(builder.target.message).toBe("Unit Test message"); 60 | expect(builder.target.data).toBeUndefined(); 61 | }); 62 | 63 | test("should accept source and message and level", () => { 64 | const client = new ExceptionlessClient(); 65 | client.config.apiKey = "UNIT_TEST_API_KEY"; 66 | const builder = client.createLog("source", "Unit Test message", "info"); 67 | 68 | expect(builder.target.source).toBe("source"); 69 | expect(builder.target.message).toBe("Unit Test message"); 70 | expect(builder.target.data?.[KnownEventDataKeys.Level]).toBe("info"); 71 | }); 72 | 73 | test("should allow construction via a configuration object", () => { 74 | const client = new ExceptionlessClient(); 75 | client.config.apiKey = "UNIT_TEST_API_KEY"; 76 | client.config.serverUrl = "https://localhost:5100"; 77 | 78 | expect(client.config.apiKey).toBe("UNIT_TEST_API_KEY"); 79 | expect(client.config.serverUrl).toBe("https://localhost:5100"); 80 | }); 81 | 82 | test("should allow construction via a constructor", async () => { 83 | const client = new ExceptionlessClient(); 84 | 85 | await client.startup((c) => { 86 | c.apiKey = "UNIT_TEST_API_KEY"; 87 | c.serverUrl = "https://localhost:5100"; 88 | c.updateSettingsWhenIdleInterval = -1; 89 | }); 90 | 91 | await client.suspend(); 92 | expect(client.config.apiKey).toBe("UNIT_TEST_API_KEY"); 93 | expect(client.config.serverUrl).toBe("https://localhost:5100"); 94 | }); 95 | 96 | function createException(): ReferenceError { 97 | function throwError() { 98 | throw new ReferenceError("This is a test"); 99 | } 100 | try { 101 | throwError(); 102 | } catch (e) { 103 | return e as ReferenceError; 104 | } 105 | 106 | return new ReferenceError("No Stack Trace"); 107 | } 108 | }); 109 | -------------------------------------------------------------------------------- /packages/core/test/configuration/Configuration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { Configuration } from "../../src/configuration/Configuration.js"; 5 | 6 | describe("Configuration", () => { 7 | test("should override configuration defaults", () => { 8 | let config = new Configuration(); 9 | expect(config.apiKey).toEqual(""); 10 | 11 | config.apiKey = "UNIT_TEST_API_KEY"; 12 | expect(config.apiKey).toBe("UNIT_TEST_API_KEY"); 13 | expect(config.includePrivateInformation).toBe(true); 14 | expect(config.includeUserName).toBe(true); 15 | expect(config.includeMachineName).toBe(true); 16 | expect(config.includeIpAddress).toBe(true); 17 | expect(config.includeCookies).toBe(true); 18 | expect(config.includePostData).toBe(true); 19 | expect(config.includeQueryString).toBe(true); 20 | 21 | config = new Configuration(); 22 | config.includePrivateInformation = false; 23 | expect(config.includePrivateInformation).toBe(false); 24 | expect(config.includeUserName).toBe(false); 25 | expect(config.includeMachineName).toBe(false); 26 | expect(config.includeIpAddress).toBe(false); 27 | expect(config.includeCookies).toBe(false); 28 | expect(config.includePostData).toBe(false); 29 | expect(config.includeQueryString).toBe(false); 30 | 31 | config.includeMachineName = true; 32 | expect(config.includePrivateInformation).toBe(false); 33 | expect(config.includeUserName).toBe(false); 34 | expect(config.includeMachineName).toBe(true); 35 | expect(config.includeIpAddress).toBe(false); 36 | expect(config.includeCookies).toBe(false); 37 | expect(config.includePostData).toBe(false); 38 | expect(config.includeQueryString).toBe(false); 39 | }); 40 | 41 | test("should not add duplicate plugin", () => { 42 | const config = new Configuration(); 43 | config.apiKey = "UNIT_TEST_API_KEY"; 44 | 45 | expect(config.plugins).not.toBeNull(); 46 | while (config.plugins.length > 0) { 47 | config.removePlugin(config.plugins[0]); 48 | } 49 | 50 | config.addPlugin("test", 20, () => Promise.resolve()); 51 | config.addPlugin("test", 20, () => Promise.resolve()); 52 | expect(config.plugins.length).toBe(1); 53 | }); 54 | 55 | test("should generate plugin name and priority", () => { 56 | const config = new Configuration(); 57 | config.apiKey = "UNIT_TEST_API_KEY"; 58 | 59 | expect(config.plugins).not.toBeNull(); 60 | while (config.plugins.length > 0) { 61 | config.removePlugin(config.plugins[0]); 62 | } 63 | 64 | config.addPlugin(undefined, NaN, () => Promise.resolve()); 65 | expect(config.plugins.length).toBe(1); 66 | expect(config.plugins[0].name).not.toBeNull(); 67 | expect(config.plugins[0].priority).toBe(0); 68 | }); 69 | 70 | test("should sort plugins by priority", () => { 71 | const config = new Configuration(); 72 | config.apiKey = "UNIT_TEST_API_KEY"; 73 | 74 | expect(config.plugins).not.toBeNull(); 75 | while (config.plugins.length > 0) { 76 | config.removePlugin(config.plugins[0]); 77 | } 78 | 79 | config.addPlugin("3", 3, () => Promise.resolve()); 80 | config.addPlugin("1", 1, () => Promise.resolve()); 81 | config.addPlugin("2", 2, () => Promise.resolve()); 82 | expect(config.plugins.length).toBe(3); 83 | expect(config.plugins[0].priority).toBe(1); 84 | expect(config.plugins[1].priority).toBe(2); 85 | expect(config.plugins[2].priority).toBe(3); 86 | }); 87 | 88 | test("should call subscribe handler", (done) => { 89 | const config = new Configuration(); 90 | expect(config.settings.someValue).toBeUndefined(); 91 | 92 | config.subscribeServerSettingsChange((c: Configuration) => { 93 | expect(c.settings.someValue).toBe("UNIT_TEST_API_KEY"); 94 | expect(config.settings.someValue).toBe("UNIT_TEST_API_KEY"); 95 | done(); 96 | }); 97 | 98 | config.applyServerSettings({ settings: { someValue: "UNIT_TEST_API_KEY" }, version: 2 }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /packages/core/test/helpers.ts: -------------------------------------------------------------------------------- 1 | export function delay(ms: number): Promise { 2 | return new Promise((r) => setTimeout(r, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/test/plugins/EventPluginManager.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { EventContext } from "../../src/models/EventContext.js"; 5 | import { ExceptionlessClient } from "../../src/ExceptionlessClient.js"; 6 | import { EventPluginContext } from "../../src/plugins/EventPluginContext.js"; 7 | import { EventPluginManager } from "../../src/plugins/EventPluginManager.js"; 8 | import { delay } from "../helpers.js"; 9 | 10 | describe("EventPluginManager", () => { 11 | test("should add items to the event.", async () => { 12 | const client = new ExceptionlessClient(); 13 | client.config.apiKey = "UNIT_TEST_API_KEY"; 14 | 15 | const context = new EventPluginContext(client, {}, new EventContext()); 16 | expect(context.event.source).toBeUndefined(); 17 | expect(context.event.geo).toBeUndefined(); 18 | 19 | expect(client.config.plugins).not.toBeNull(); 20 | while (client.config.plugins.length > 0) { 21 | client.config.removePlugin(client.config.plugins[0]); 22 | } 23 | 24 | client.config.addPlugin("1", 1, async (ctx: EventPluginContext) => { 25 | await delay(25); 26 | ctx.event.source = "plugin 1"; 27 | }); 28 | 29 | client.config.addPlugin("2", 2, (ctx: EventPluginContext) => { 30 | ctx.event.geo = "43.5775,88.4472"; 31 | return Promise.resolve(); 32 | }); 33 | 34 | await EventPluginManager.run(context); 35 | expect(context.cancelled).toBe(false); 36 | expect(context.event.source).toBe("plugin 1"); 37 | expect(context.event.geo).toBe("43.5775,88.4472"); 38 | }); 39 | 40 | test("setting cancel should stop plugin execution.", async () => { 41 | const client = new ExceptionlessClient(); 42 | client.config.apiKey = "UNIT_TEST_API_KEY"; 43 | 44 | const context = new EventPluginContext(client, {}, new EventContext()); 45 | expect(client.config.plugins).not.toBeNull(); 46 | while (client.config.plugins.length > 0) { 47 | client.config.removePlugin(client.config.plugins[0]); 48 | } 49 | 50 | client.config.addPlugin("1", 1, (ctx: EventPluginContext) => { 51 | ctx.cancelled = true; 52 | return Promise.resolve(); 53 | }); 54 | 55 | client.config.addPlugin("2", 2, () => { 56 | throw new Error("Plugin should not be called due to cancellation"); 57 | }); 58 | 59 | await EventPluginManager.run(context); 60 | expect(context.cancelled).toBe(true); 61 | }); 62 | 63 | test("throwing error should stop plugin execution.", async () => { 64 | const client = new ExceptionlessClient(); 65 | client.config.apiKey = "UNIT_TEST_API_KEY"; 66 | const context = new EventPluginContext(client, {}, new EventContext()); 67 | 68 | expect(client.config.plugins).not.toBeNull(); 69 | while (client.config.plugins.length > 0) { 70 | client.config.removePlugin(client.config.plugins[0]); 71 | } 72 | 73 | client.config.addPlugin("1", 1, () => { 74 | throw new Error("Random Error"); 75 | }); 76 | 77 | client.config.addPlugin("2", 2, () => { 78 | throw new Error("Plugin should not be called due to cancellation"); 79 | }); 80 | 81 | await EventPluginManager.run(context); 82 | expect(context.cancelled).toBe(true); 83 | }); 84 | 85 | test("should cancel via timeout.", async () => { 86 | const client = new ExceptionlessClient(); 87 | client.config.apiKey = "UNIT_TEST_API_KEY"; 88 | const context = new EventPluginContext(client, {}, new EventContext()); 89 | 90 | expect(client.config.plugins).not.toBeNull(); 91 | while (client.config.plugins.length > 0) { 92 | client.config.removePlugin(client.config.plugins[0]); 93 | } 94 | 95 | client.config.addPlugin("1", 1, async () => { 96 | await delay(25); 97 | }); 98 | 99 | client.config.addPlugin("2", 2, () => { 100 | throw new Error("Plugin should not be called due to cancellation"); 101 | }); 102 | 103 | await EventPluginManager.run(context); 104 | expect(context.cancelled).toBe(true); 105 | }); 106 | 107 | test("should ensure config plugins are not wrapped.", async () => { 108 | const client = new ExceptionlessClient(); 109 | client.config.apiKey = "UNIT_TEST_API_KEY"; 110 | const context = new EventPluginContext(client, {}, new EventContext()); 111 | 112 | expect(client.config.plugins).not.toBeNull(); 113 | while (client.config.plugins.length > 0) { 114 | client.config.removePlugin(client.config.plugins[0]); 115 | } 116 | 117 | client.config.addPlugin("1", 1, () => { 118 | return Promise.resolve(); 119 | }); 120 | 121 | expect(client.config.plugins[0].name).toBe("1"); 122 | expect(client.config.plugins.length).toBe(1); 123 | 124 | await EventPluginManager.run(context); 125 | expect(client.config.plugins[0].name).toBe("1"); 126 | expect(client.config.plugins.length).toBe(1); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /packages/core/test/plugins/default/ConfigurationDefaultsPlugin.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { ExceptionlessClient } from "../../../src/ExceptionlessClient.js"; 5 | import { Event } from "../../../src/models/Event.js"; 6 | import { ConfigurationDefaultsPlugin } from "../../../src/plugins/default/ConfigurationDefaultsPlugin.js"; 7 | import { EventPluginContext } from "../../../src/plugins/EventPluginContext.js"; 8 | import { EventContext } from "../../../src/models/EventContext.js"; 9 | 10 | describe("ConfigurationDefaultsPlugin", () => { 11 | describe("should add default", () => { 12 | const userDataKey: string = "user"; 13 | const user = { 14 | id: 1, 15 | name: "Blake", 16 | password: "123456", 17 | passwordResetToken: "a reset token", 18 | myPassword: "123456", 19 | myPasswordValue: "123456", 20 | customValue: "Password", 21 | value: { 22 | Password: "123456" 23 | } 24 | }; 25 | 26 | const defaultTags: string[] = ["tag1", "tag2"]; 27 | 28 | const run = async (dataExclusions?: string[] | undefined): Promise => { 29 | const client = new ExceptionlessClient(); 30 | client.config.defaultTags.push(...defaultTags); 31 | client.config.defaultData[userDataKey] = user; 32 | 33 | if (dataExclusions) { 34 | client.config.addDataExclusions(...dataExclusions); 35 | } 36 | 37 | const ev: Event = { type: "log", source: "test", data: {} }; 38 | 39 | const context = new EventPluginContext(client, ev, new EventContext()); 40 | const plugin = new ConfigurationDefaultsPlugin(); 41 | await plugin.run(context); 42 | 43 | return context.event; 44 | }; 45 | 46 | test("tags", async () => { 47 | const ev = await run(); 48 | expect(ev.tags).toStrictEqual(defaultTags); 49 | }); 50 | 51 | test("user", async () => { 52 | const ev = await run(); 53 | expect(ev.data).toBeDefined(); 54 | expect(ev.data && ev.data[userDataKey]).toStrictEqual(user); 55 | }); 56 | 57 | test("pruned user", async () => { 58 | const ev = await run(["*password*"]); 59 | expect(ev.data).toBeDefined(); 60 | 61 | const expected = { id: 1, name: "Blake", customValue: "Password", value: {} }; 62 | expect(ev.data && ev.data[userDataKey]).toStrictEqual(expected); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/core/test/plugins/default/DuplicateCheckerPlugin.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { DuplicateCheckerPlugin } from "../../../src/plugins/default/DuplicateCheckerPlugin.js"; 5 | import { ExceptionlessClient } from "../../../src/ExceptionlessClient.js"; 6 | import { EventPluginContext } from "../../../src/plugins/EventPluginContext.js"; 7 | import { InnerErrorInfo, StackFrameInfo } from "../../../src/models/data/ErrorInfo.js"; 8 | import { delay } from "../../helpers.js"; 9 | import { EventContext } from "../../../src/models/EventContext.js"; 10 | 11 | const Exception1StackTrace = [ 12 | { 13 | file_name: "index.js", 14 | line_number: 0, 15 | column: 50, 16 | is_signature_target: true, 17 | name: "createException" 18 | }, 19 | { 20 | file_name: "index.js", 21 | line_number: 5, 22 | column: 25, 23 | is_signature_target: false, 24 | name: "throwError" 25 | } 26 | ]; 27 | 28 | const Exception2StackTrace = [ 29 | { 30 | file_name: "index.js", 31 | line_number: 0, 32 | column: 50, 33 | is_signature_target: true, 34 | name: "createException2" 35 | }, 36 | { 37 | file_name: "index.js", 38 | line_number: 5, 39 | column: 25, 40 | is_signature_target: false, 41 | name: "throwError2" 42 | } 43 | ]; 44 | 45 | describe("DuplicateCheckerPlugin", () => { 46 | let now: number = 0; 47 | let client: ExceptionlessClient; 48 | let plugin: DuplicateCheckerPlugin; 49 | 50 | beforeEach(() => { 51 | client = new ExceptionlessClient(); 52 | plugin = new DuplicateCheckerPlugin(() => now, 50); 53 | }); 54 | 55 | const run = async (stackTrace?: StackFrameInfo[]): Promise => { 56 | // TODO: Generate unique stack traces based on test data. 57 | const context = new EventPluginContext( 58 | client, 59 | { 60 | type: "error", 61 | data: { 62 | "@error": { 63 | type: "ReferenceError", 64 | message: "This is a test", 65 | stack_trace: stackTrace 66 | } 67 | } 68 | }, 69 | new EventContext() 70 | ); 71 | 72 | await plugin.run(context); 73 | return context; 74 | }; 75 | 76 | test("should ignore duplicate within window", async () => { 77 | await run(Exception1StackTrace); 78 | 79 | const contextOfSecondRun = await run(Exception1StackTrace); 80 | expect(contextOfSecondRun.cancelled).toBe(true); 81 | await delay(100); 82 | setTimeout(() => { 83 | expect(contextOfSecondRun.event.count).toBe(1); 84 | }, 100); 85 | }); 86 | 87 | test("should ignore error without stack", async () => { 88 | await run(); 89 | const contextOfSecondRun = await run(); 90 | expect(contextOfSecondRun.cancelled).toBe(true); 91 | }); 92 | 93 | test("shouldn't ignore different stack within window", async () => { 94 | await run(Exception1StackTrace); 95 | const contextOfSecondRun = await run(Exception2StackTrace); 96 | 97 | expect(contextOfSecondRun.cancelled).not.toBe(true); 98 | }); 99 | 100 | test("shouldn't ignore duplicate after window", async () => { 101 | await run(Exception1StackTrace); 102 | 103 | now = 3000; 104 | const contextOfSecondRun = await run(Exception1StackTrace); 105 | expect(contextOfSecondRun.cancelled).not.toBe(true); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /packages/core/test/queue/DefaultEventQueue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { Configuration } from "../../src/configuration/Configuration.js"; 5 | import { Event } from "../../src/models/Event.js"; 6 | import { DefaultEventQueue } from "../../src/queue/DefaultEventQueue.js"; 7 | import { delay } from "../helpers.js"; 8 | 9 | describe("DefaultEventQueue", () => { 10 | let config: Configuration; 11 | 12 | beforeEach(async () => { 13 | config = new Configuration(); 14 | config.apiKey = "UNIT_TEST_API_KEY"; 15 | config.serverUrl = "http://server.localhost:5000"; 16 | config.usePersistedQueueStorage = true; 17 | 18 | expect(await config.services.storage.length()).toBe(0); 19 | }); 20 | 21 | afterEach(async () => { 22 | const queue = config.services.queue; 23 | await queue.suspend(); 24 | }); 25 | 26 | test("should enqueue event", async () => { 27 | const event: Event = { type: "log", reference_id: "123454321" }; 28 | await config.services.queue.enqueue(event); 29 | expect(await config.services.storage.length()).toBe(1); 30 | }); 31 | 32 | test("should process queue", async () => { 33 | const event: Event = { type: "log", reference_id: "123454321" }; 34 | await config.services.queue.enqueue(event); 35 | expect(await config.services.storage.length()).toBe(1); 36 | await config.services.queue.process(); 37 | 38 | config.services.queue.onEventsPosted(async () => { 39 | expect((config.services.queue as { _suspendProcessingUntil?: Date })._suspendProcessingUntil).toBeUndefined(); 40 | expect(await config.services.storage.length()).toBe(0); 41 | }); 42 | }); 43 | 44 | test("should discard event submission", async () => { 45 | await config.services.queue.suspendProcessing(1, true); 46 | 47 | const event: Event = { type: "log", reference_id: "123454321" }; 48 | await config.services.queue.enqueue(event); 49 | expect(await config.services.storage.length()).toBe(0); 50 | }); 51 | 52 | test("should suspend processing", async () => { 53 | await config.services.queue.suspendProcessing(0.0001); 54 | 55 | const event: Event = { type: "log", reference_id: "123454321" }; 56 | await config.services.queue.enqueue(event); 57 | expect(await config.services.storage.length()).toBe(1); 58 | 59 | await delay(25); 60 | if (!(config.services.queue as { _suspendProcessingUntil?: Date })._suspendProcessingUntil) { 61 | expect(await config.services.storage.length()).toBe(0); 62 | } else { 63 | expect(await config.services.storage.length()).toBe(1); 64 | } 65 | }); 66 | 67 | test("should respect max items", async () => { 68 | config.services.queue = new DefaultEventQueue(config, 1); 69 | const event: Event = { type: "log", reference_id: "123454321" }; 70 | for (let index = 0; index < 2; index++) { 71 | await config.services.queue.enqueue(event); 72 | expect(await config.services.storage.length()).toBe(1); 73 | } 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/core/test/storage/InMemoryStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryStorage } from "../../src/storage/InMemoryStorage.js"; 2 | import { IStorage } from "../../src/storage/IStorage.js"; 3 | import { describeStorage } from "./StorageTestBase.js"; 4 | 5 | describeStorage("InMemoryStorage", (): IStorage => new InMemoryStorage()); 6 | -------------------------------------------------------------------------------- /packages/core/test/storage/LocalStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "../../src/storage/IStorage.js"; 2 | import { describeStorage } from "./StorageTestBase.js"; 3 | import { LocalStorage } from "../../src/storage/LocalStorage.js"; 4 | 5 | function resetLocalStorage() { 6 | localStorage.clear(); 7 | } 8 | 9 | describeStorage("LocalStorage", (): IStorage => new LocalStorage(), resetLocalStorage, resetLocalStorage); 10 | -------------------------------------------------------------------------------- /packages/core/test/storage/StorageTestBase.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { IStorage } from "../../src/storage/IStorage.js"; 5 | 6 | export function describeStorage(name: string, storageFactory: () => IStorage, afterEachCallback?: () => void, beforeEachCallback?: () => void): void { 7 | describe(name, () => { 8 | if (beforeEachCallback) { 9 | beforeEach(beforeEachCallback); 10 | } 11 | 12 | if (afterEachCallback) { 13 | afterEach(afterEachCallback); 14 | } 15 | 16 | test("can save item", async () => { 17 | const storage = storageFactory(); 18 | expect(await storage.length()).toBe(0); 19 | expect(await storage.keys()).toEqual([]); 20 | expect(await storage.key(0)).toBeNull(); 21 | 22 | const file: string = "event.json"; 23 | const value: string = "test"; 24 | 25 | await storage.setItem(file, value); 26 | expect(await storage.length()).toBe(1); 27 | expect(await storage.getItem(file)).toEqual(value); 28 | expect(await storage.keys()).toEqual([file]); 29 | expect(await storage.key(0)).toEqual(file); 30 | }); 31 | 32 | test("can remove item", async () => { 33 | const storage = storageFactory(); 34 | expect(await storage.length()).toBe(0); 35 | expect(await storage.keys()).toEqual([]); 36 | expect(await storage.key(0)).toBeNull(); 37 | 38 | const file: string = "event.json"; 39 | const value: string = "test"; 40 | 41 | await storage.setItem(file, value); 42 | expect(await storage.length()).toBe(1); 43 | expect(await storage.keys()).toEqual([file]); 44 | expect(await storage.key(0)).toEqual(file); 45 | 46 | await storage.removeItem(file); 47 | expect(await storage.length()).toBe(0); 48 | expect(await storage.keys()).toEqual([]); 49 | expect(await storage.key(0)).toBeNull(); 50 | }); 51 | 52 | test("can clear", async () => { 53 | const storage = storageFactory(); 54 | expect(await storage.length()).toBe(0); 55 | expect(await storage.keys()).toEqual([]); 56 | expect(await storage.key(0)).toBeNull(); 57 | 58 | await storage.setItem("event.json", "test"); 59 | expect(await storage.length()).toBe(1); 60 | 61 | await storage.clear(); 62 | expect(await storage.length()).toBe(0); 63 | expect(await storage.keys()).toEqual([]); 64 | expect(await storage.key(0)).toBeNull(); 65 | }); 66 | 67 | test("can handle missing files", async () => { 68 | const storage = storageFactory(); 69 | expect(await storage.length()).toBe(0); 70 | expect(await storage.keys()).toEqual([]); 71 | expect(await storage.key(0)).toBeNull(); 72 | 73 | const file: string = "event.json"; 74 | const value: string = "test"; 75 | 76 | await storage.setItem(file, value); 77 | expect(await storage.length()).toBe(1); 78 | expect(await storage.keys()).toEqual([file]); 79 | expect(await storage.key(0)).toEqual(file); 80 | 81 | await storage.removeItem("random.json"); 82 | expect(await storage.length()).toBe(1); 83 | expect(await storage.keys()).toEqual([file]); 84 | expect(await storage.key(0)).toEqual(file); 85 | 86 | expect(await storage.key(1)).toBeNull(); 87 | expect(await storage.getItem("random.json")).toBeNull(); 88 | }); 89 | // what to do about null or undefined keys? 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /packages/core/test/submission/TestSubmissionClient.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, jest, test } from "@jest/globals"; 2 | import { expect } from "expect"; 3 | 4 | import { Configuration } from "../../src/configuration/Configuration.js"; 5 | import { ServerSettings } from "../../src/configuration/SettingsManager.js"; 6 | import { Event } from "../../src/models/Event.js"; 7 | import { UserDescription } from "../../src/models/data/UserDescription.js"; 8 | import { Response } from "../../src/submission/Response.js"; 9 | import { TestSubmissionClient } from "./TestSubmissionClient.js"; 10 | import { FetchOptions } from "../../src/submission/DefaultSubmissionClient.js"; 11 | 12 | describe("TestSubmissionClient", () => { 13 | const config: Configuration = new Configuration(); 14 | config.apiKey = "UNIT_TEST_API_KEY"; 15 | config.serverUrl = "http://server.localhost:5000"; 16 | config.configServerUrl = "http://config.localhost:5000"; 17 | config.heartbeatServerUrl = "http://heartbeat.localhost:5000"; 18 | 19 | test("should submit events", async () => { 20 | const apiFetchMock = jest 21 | .fn<(url: string, options: FetchOptions) => Promise>>() 22 | .mockReturnValueOnce(Promise.resolve(new Response(202, "", NaN, NaN, undefined))); 23 | 24 | const events: Event[] = [{ type: "log", message: "From js client", reference_id: "123454321" }]; 25 | const client = new TestSubmissionClient(config, apiFetchMock); 26 | await client.submitEvents(events); 27 | expect(apiFetchMock).toHaveBeenCalledTimes(1); 28 | expect(apiFetchMock.mock.calls[0][0]).toBe(`${config.serverUrl}/api/v2/events`); 29 | expect(apiFetchMock.mock.calls[0][1]).toEqual({ 30 | method: "POST", 31 | body: JSON.stringify(events) 32 | }); 33 | }); 34 | 35 | test("should submit invalid object data", async () => { 36 | const apiFetchMock = jest 37 | .fn<(url: string, options: FetchOptions) => Promise>>() 38 | .mockReturnValueOnce(Promise.resolve(new Response(202, "", NaN, NaN, undefined))); 39 | 40 | const events: Event[] = [ 41 | { 42 | type: "log", 43 | message: "From js client", 44 | reference_id: "123454321", 45 | data: { 46 | name: "blake", 47 | age: () => { 48 | throw new Error("Test"); 49 | } 50 | } 51 | } 52 | ]; 53 | 54 | const client = new TestSubmissionClient(config, apiFetchMock); 55 | await client.submitEvents(events); 56 | expect(apiFetchMock).toHaveBeenCalledTimes(1); 57 | expect(apiFetchMock.mock.calls[0][0]).toBe(`${config.serverUrl}/api/v2/events`); 58 | expect(apiFetchMock.mock.calls[0][1]).toEqual({ 59 | method: "POST", 60 | body: JSON.stringify(events) 61 | }); 62 | }); 63 | 64 | test("should submit user description", async () => { 65 | const apiFetchMock = jest 66 | .fn<(url: string, options: FetchOptions) => Promise>>() 67 | .mockReturnValueOnce(Promise.resolve(new Response(202, "", NaN, 1, undefined))) 68 | .mockReturnValueOnce(Promise.resolve(new Response(202, "", NaN, NaN, JSON.stringify(new ServerSettings({}, 1))))); 69 | 70 | const description: UserDescription = { 71 | email_address: "norply@exceptionless.io", 72 | description: "unit test" 73 | }; 74 | 75 | const client = (config.services.submissionClient = new TestSubmissionClient(config, apiFetchMock)); 76 | await client.submitUserDescription("123454321", description); 77 | expect(apiFetchMock).toHaveBeenCalledTimes(2); 78 | expect(apiFetchMock.mock.calls[0][0]).toBe(`${config.serverUrl}/api/v2/events/by-ref/123454321/user-description`); 79 | expect(apiFetchMock.mock.calls[0][1]).toEqual({ 80 | method: "POST", 81 | body: JSON.stringify(description) 82 | }); 83 | expect(apiFetchMock.mock.calls[1][0]).toBe(`${config.serverUrl}/api/v2/projects/config?v=0`); 84 | expect(apiFetchMock.mock.calls[1][1]).toEqual({ method: "GET" }); 85 | }); 86 | 87 | test("should submit heartbeat", async () => { 88 | const apiFetchMock = jest 89 | .fn<(url: string, options: FetchOptions) => Promise>>() 90 | .mockReturnValueOnce(Promise.resolve(new Response(200, "", NaN, NaN, undefined))); 91 | 92 | const client = new TestSubmissionClient(config, apiFetchMock); 93 | await client.submitHeartbeat("sessionId", true); 94 | expect(apiFetchMock).toHaveBeenCalledTimes(1); 95 | expect(apiFetchMock.mock.calls[0][0]).toBe(`${config.heartbeatServerUrl}/api/v2/events/session/heartbeat?id=sessionId&close=true`); 96 | expect(apiFetchMock.mock.calls[0][1]).toEqual({ method: "GET" }); 97 | }); 98 | 99 | test("should get project settings", async () => { 100 | const apiFetchMock = jest 101 | .fn<(url: string, options: FetchOptions) => Promise>>() 102 | .mockReturnValueOnce(Promise.resolve(new Response(200, "", NaN, NaN, JSON.stringify(new ServerSettings({}, 1))))); 103 | 104 | const client = new TestSubmissionClient(config, apiFetchMock); 105 | await client.getSettings(0); 106 | expect(apiFetchMock).toHaveBeenCalledTimes(1); 107 | expect(apiFetchMock.mock.calls[0][0]).toBe(`${config.serverUrl}/api/v2/projects/config?v=0`); 108 | expect(apiFetchMock.mock.calls[0][1]).toEqual({ method: "GET" }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /packages/core/test/submission/TestSubmissionClient.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "../../src/configuration/Configuration.js"; 2 | import { DefaultSubmissionClient, FetchOptions } from "../../src/submission/DefaultSubmissionClient.js"; 3 | import { Response } from "../../src/submission/Response.js"; 4 | 5 | export type ApiFetchMock = (url: string, options: FetchOptions) => Promise>; 6 | 7 | export class TestSubmissionClient extends DefaultSubmissionClient { 8 | public constructor( 9 | protected config: Configuration, 10 | protected apiFetchMock: ApiFetchMock 11 | ) { 12 | super(config); 13 | } 14 | 15 | protected async apiFetch(url: string, options: FetchOptions): Promise> { 16 | if (!this.apiFetchMock) { 17 | throw new Error("Missing mock"); 18 | } 19 | 20 | const response = await this.apiFetchMock(url, options); 21 | return response as Response; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "types": ["jest"] 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/node/README.md: -------------------------------------------------------------------------------- 1 | # Exceptionless NodeJS 2 | 3 | Using Exceptionless in the NodeJS environment is similar to using it in other 4 | JavaScript environments. 5 | 6 | ## Getting Started 7 | 8 | To use this package, your must be using ES6 and Node 18+. 9 | 10 | ## Installation 11 | 12 | `npm install @exceptionless/node --save` 13 | 14 | ## Configuration 15 | 16 | While your app is starting up, you should call `startup` on the Exceptionless 17 | client. This ensures the client is configured and automatic capturing of 18 | unhandled errors occurs. 19 | 20 | ```js 21 | import { Exceptionless } from "@exceptionless/node"; 22 | 23 | await Exceptionless.startup((c) => { 24 | c.apiKey = "API_KEY_HERE"; 25 | 26 | // set some default data 27 | c.defaultData["mydata"] = { 28 | myGreeting: "Hello World" 29 | }; 30 | 31 | c.defaultTags.push("Example", "JavaScript", "Node"); 32 | }); 33 | ``` 34 | 35 | Once that's done, you can use the Exceptionless client anywhere in your app by 36 | importing `Exceptionless` followed by the method you want to use. For example: 37 | 38 | ```js 39 | await Exceptionless.submitLog("Hello world!"); 40 | ``` 41 | 42 | Please see the [docs](https://exceptionless.com/docs/clients/javascript/) for 43 | more information on configuring the client. 44 | 45 | ### Source Maps 46 | 47 | For improved stack traces launch your Node app with the 48 | [`--enable-source-maps` command line option](https://nodejs.org/docs/latest-v18.x/api/cli.html#--enable-source-maps). 49 | 50 | ```sh 51 | node app.js --enable-source-maps 52 | ``` 53 | 54 | ## Support 55 | 56 | If you need help, please contact us via in-app support, 57 | [open an issue](https://github.com/exceptionless/Exceptionless.JavaScript/issues/new) 58 | or [join our chat on Discord](https://discord.gg/6HxgFCx). We’re always here to 59 | help if you have any questions! 60 | -------------------------------------------------------------------------------- /packages/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exceptionless/node", 3 | "version": "3.0.0-dev", 4 | "description": "JavaScript client for Exceptionless", 5 | "author": { 6 | "name": "exceptionless", 7 | "url": "https://exceptionless.com" 8 | }, 9 | "keywords": [ 10 | "exceptionless", 11 | "error", 12 | "feature", 13 | "logging", 14 | "tracking", 15 | "reporting", 16 | "node" 17 | ], 18 | "repository": { 19 | "url": "git://github.com/exceptionless/Exceptionless.JavaScript.git", 20 | "type": "git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/exceptionless/Exceptionless.JavaScript/issues" 24 | }, 25 | "license": "Apache-2.0", 26 | "type": "module", 27 | "main": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "unpkg": "dist/index.bundle.min.js", 30 | "jsdelivr": "dist/index.bundle.min.js", 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./package.json": "./package.json" 34 | }, 35 | "engines": { 36 | "node": ">=18" 37 | }, 38 | "jest": { 39 | "moduleFileExtensions": [ 40 | "js", 41 | "ts" 42 | ], 43 | "moduleNameMapper": { 44 | "^@exceptionless/(.*)$": "/../$1/src" 45 | }, 46 | "preset": "ts-jest", 47 | "resolver": "jest-ts-webcompat-resolver", 48 | "testEnvironment": "node" 49 | }, 50 | "scripts": { 51 | "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --platform=node --format=esm --outfile=dist/index.bundle.js", 52 | "watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --platform=node --sourcemap --format=esm --watch --outfile=dist/index.bundle.js", 53 | "test": "jest", 54 | "test:watch": "jest --watch" 55 | }, 56 | "sideEffects": false, 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "devDependencies": { 61 | "@jest/globals": "^29.7.0", 62 | "@types/node": "^20.11.30", 63 | "@types/node-localstorage": "^1.3.3", 64 | "@types/stack-trace": "^0.0.33", 65 | "esbuild": "^0.20.2", 66 | "jest": "^29.7.0", 67 | "jest-ts-webcompat-resolver": "^1.0.0", 68 | "ts-jest": "^29.1.2" 69 | }, 70 | "dependencies": { 71 | "@exceptionless/core": "3.0.0-dev", 72 | "node-localstorage": "^3.0.5", 73 | "stack-trace": "^1.0.0-pre2" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/node/src/NodeExceptionlessClient.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, ExceptionlessClient, LocalStorage, SimpleErrorPlugin } from "@exceptionless/core"; 2 | 3 | import { LocalStorage as LocalStoragePolyfill } from "node-localstorage"; 4 | 5 | import { NodeErrorPlugin } from "./plugins/NodeErrorPlugin.js"; 6 | import { NodeEnvironmentInfoPlugin } from "./plugins/NodeEnvironmentInfoPlugin.js"; 7 | import { NodeGlobalHandlerPlugin } from "./plugins/NodeGlobalHandlerPlugin.js"; 8 | import { NodeLifeCyclePlugin } from "./plugins/NodeLifeCyclePlugin.js"; 9 | import { NodeRequestInfoPlugin } from "./plugins/NodeRequestInfoPlugin.js"; 10 | import { NodeWrapFunctions } from "./plugins/NodeWrapFunctions.js"; 11 | 12 | export class NodeExceptionlessClient extends ExceptionlessClient { 13 | public async startup(configurationOrApiKey?: (config: Configuration) => void | string): Promise { 14 | const config = this.config; 15 | 16 | if (configurationOrApiKey && !this._initialized) { 17 | try { 18 | const storage = new LocalStorage(undefined, new LocalStoragePolyfill(process.cwd() + "/.exceptionless")); 19 | config.useLocalStorage = () => storage; 20 | config.services.storage = storage; 21 | } catch (ex) { 22 | this.config.services.log.info(`Error configuring localStorage polyfill: ${ex instanceof Error ? ex.message : ex + ""}`); 23 | } 24 | 25 | config.addPlugin(new NodeEnvironmentInfoPlugin()); 26 | config.addPlugin(new NodeGlobalHandlerPlugin()); 27 | config.addPlugin(new NodeLifeCyclePlugin()); 28 | config.addPlugin(new NodeRequestInfoPlugin()); 29 | config.addPlugin(new NodeWrapFunctions()); 30 | config.addPlugin(new NodeErrorPlugin()); 31 | config.removePlugin(new SimpleErrorPlugin()); 32 | } 33 | 34 | await super.startup(configurationOrApiKey); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/node/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@exceptionless/core"; 2 | 3 | export { NodeErrorPlugin } from "./plugins/NodeErrorPlugin.js"; 4 | export { NodeEnvironmentInfoPlugin } from "./plugins/NodeEnvironmentInfoPlugin.js"; 5 | export { NodeGlobalHandlerPlugin } from "./plugins/NodeGlobalHandlerPlugin.js"; 6 | export { NodeLifeCyclePlugin } from "./plugins/NodeLifeCyclePlugin.js"; 7 | export { NodeRequestInfoPlugin } from "./plugins/NodeRequestInfoPlugin.js"; 8 | export { NodeWrapFunctions } from "./plugins/NodeWrapFunctions.js"; 9 | export { NodeExceptionlessClient } from "./NodeExceptionlessClient.js"; 10 | 11 | import { NodeExceptionlessClient } from "./NodeExceptionlessClient.js"; 12 | 13 | export const Exceptionless = new NodeExceptionlessClient(); 14 | -------------------------------------------------------------------------------- /packages/node/src/plugins/NodeEnvironmentInfoPlugin.ts: -------------------------------------------------------------------------------- 1 | import { argv, memoryUsage, pid, title, version } from "process"; 2 | 3 | import { arch, cpus, endianness, freemem, hostname, loadavg, networkInterfaces, platform, release, tmpdir, totalmem, type, uptime } from "os"; 4 | 5 | import { EnvironmentInfo, EventPluginContext, IEventPlugin, KnownEventDataKeys } from "@exceptionless/core"; 6 | 7 | export class NodeEnvironmentInfoPlugin implements IEventPlugin { 8 | public priority: number = 80; 9 | public name: string = "NodeEnvironmentInfoPlugin"; 10 | private _environmentInfo: EnvironmentInfo | undefined; 11 | 12 | public run(context: EventPluginContext): Promise { 13 | if (context.event.data && !context.event.data[KnownEventDataKeys.EnvironmentInfo]) { 14 | const info: EnvironmentInfo | undefined = this.getEnvironmentInfo(context); 15 | if (info) { 16 | context.event.data[KnownEventDataKeys.EnvironmentInfo] = info; 17 | } 18 | } 19 | 20 | return Promise.resolve(); 21 | } 22 | 23 | private getEnvironmentInfo(context: EventPluginContext): EnvironmentInfo | undefined { 24 | function getIpAddresses(): string { 25 | const ips: string[] = []; 26 | 27 | for (const ni of Object.values(networkInterfaces())) { 28 | for (const network of ni || []) { 29 | if (!network.internal && "IPv4" === network.family) { 30 | ips.push(network.address); 31 | } 32 | } 33 | } 34 | 35 | return ips.join(", "); 36 | } 37 | 38 | function populateMemoryAndUptimeInfo(ei: EnvironmentInfo) { 39 | ei.process_memory_size = memoryUsage().heapTotal; 40 | ei.total_physical_memory = totalmem(); 41 | ei.available_physical_memory = freemem(); 42 | (ei.data as Record).loadavg = loadavg(); 43 | (ei.data as Record).uptime = uptime(); 44 | } 45 | 46 | if (!cpus) { 47 | return; 48 | } 49 | 50 | if (this._environmentInfo) { 51 | populateMemoryAndUptimeInfo(this._environmentInfo); 52 | return this._environmentInfo; 53 | } 54 | 55 | const info: EnvironmentInfo = { 56 | processor_count: cpus().length, 57 | command_line: argv.join(" "), 58 | process_name: (title || "").replace(/[\uE000-\uF8FF]/g, ""), 59 | process_id: pid + "", 60 | process_memory_size: memoryUsage().heapTotal, 61 | // thread_id: "", 62 | architecture: arch(), 63 | o_s_name: type(), 64 | o_s_version: release(), 65 | // install_id: "", 66 | runtime_version: version, 67 | data: { 68 | platform: platform(), 69 | tmpdir: tmpdir() 70 | } 71 | }; 72 | 73 | const config = context.client.config; 74 | if (config.includeMachineName) { 75 | info.machine_name = hostname(); 76 | } 77 | 78 | if (config.includeIpAddress) { 79 | info.ip_address = getIpAddresses(); 80 | } 81 | 82 | if (endianness) { 83 | (info.data as Record).endianness = endianness(); 84 | } 85 | 86 | populateMemoryAndUptimeInfo(info); 87 | 88 | this._environmentInfo = info; 89 | return this._environmentInfo; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/node/src/plugins/NodeErrorPlugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IEventPlugin, 3 | ErrorInfo, 4 | EventPluginContext, 5 | KnownEventDataKeys, 6 | IgnoredErrorProperties, 7 | isEmpty, 8 | StackFrameInfo, 9 | stringify 10 | } from "@exceptionless/core"; 11 | 12 | import { parse as fromError } from "stack-trace"; 13 | 14 | export class NodeErrorPlugin implements IEventPlugin { 15 | public priority = 30; 16 | public name = "NodeErrorPlugin"; 17 | 18 | public async run(context: EventPluginContext): Promise { 19 | const exception = context.eventContext.getException(); 20 | if (exception) { 21 | if (!context.event.type) { 22 | context.event.type = "error"; 23 | } 24 | 25 | if (context.event.data && !context.event.data[KnownEventDataKeys.Error]) { 26 | const result = await this.parse(exception); 27 | if (result) { 28 | const exclusions = context.client.config.dataExclusions.concat(IgnoredErrorProperties); 29 | const additionalData = stringify(exception, exclusions); 30 | if (!isEmpty(additionalData)) { 31 | if (!result.data) { 32 | result.data = {}; 33 | } 34 | 35 | result.data["@ext"] = JSON.parse(additionalData); 36 | } 37 | 38 | context.event.data[KnownEventDataKeys.Error] = result; 39 | } 40 | } 41 | } 42 | } 43 | 44 | private parse(exception: Error): Promise { 45 | type stackFrameShape = { 46 | methodName: string; 47 | functionName: string; 48 | fileName: string; 49 | lineNumber: number; 50 | columnNumber: number; 51 | typeName: string; 52 | native: boolean; 53 | }; 54 | 55 | function getStackFrames(stackFrames: stackFrameShape[]): StackFrameInfo[] { 56 | const frames: StackFrameInfo[] = []; 57 | 58 | for (const frame of stackFrames) { 59 | frames.push({ 60 | name: frame.methodName || frame.functionName, 61 | parameters: [], // TODO: See if there is a way to get this. 62 | file_name: frame.fileName, 63 | line_number: frame.lineNumber || 0, 64 | column: frame.columnNumber || 0, 65 | declaring_type: frame.typeName, 66 | data: { 67 | is_native: frame.native || (frame.fileName && frame.fileName[0] !== "/" && frame.fileName[0] !== ".") 68 | } 69 | }); 70 | } 71 | 72 | return frames; 73 | } 74 | 75 | const result = exception.stack ? fromError(exception) : []; 76 | if (!result) { 77 | throw new Error("Unable to parse the exception stack trace"); 78 | } 79 | 80 | return Promise.resolve({ 81 | type: exception.name || "Error", 82 | message: exception.message, 83 | stack_trace: getStackFrames((result) || []) // TODO: Update type definition. 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/node/src/plugins/NodeGlobalHandlerPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient, IEventPlugin, PluginContext, toError } from "@exceptionless/core"; 2 | 3 | export class NodeGlobalHandlerPlugin implements IEventPlugin { 4 | public priority: number = 100; 5 | public name: string = "NodeGlobalHandlerPlugin"; 6 | 7 | private _client: ExceptionlessClient | null = null; 8 | 9 | public startup(context: PluginContext): Promise { 10 | if (this._client) { 11 | return Promise.resolve(); 12 | } 13 | 14 | this._client = context.client; 15 | Error.stackTraceLimit = 50; 16 | 17 | process.addListener("uncaughtException", (error: Error) => { 18 | void this._client?.submitUnhandledException(error, "uncaughtException"); 19 | }); 20 | 21 | process.addListener("unhandledRejection", (reason: unknown) => { 22 | const error: Error = toError(reason, "Unhandled rejection"); 23 | void this._client?.submitUnhandledException(error, "unhandledRejection"); 24 | }); 25 | 26 | return Promise.resolve(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/node/src/plugins/NodeLifeCyclePlugin.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient, IEventPlugin, PluginContext } from "@exceptionless/core"; 2 | 3 | export class NodeLifeCyclePlugin implements IEventPlugin { 4 | public priority: number = 105; 5 | public name: string = "NodeLifeCyclePlugin"; 6 | 7 | private _client: ExceptionlessClient | null = null; 8 | 9 | public startup(context: PluginContext): Promise { 10 | if (this._client) { 11 | return Promise.resolve(); 12 | } 13 | 14 | this._client = context.client; 15 | 16 | let processingBeforeExit: boolean = false; 17 | process.on("beforeExit", (code: number) => { 18 | // NOTE: We need to check if we are already processing a beforeExit event 19 | // as async work will cause the runtime to call this handler again. 20 | if (processingBeforeExit) { 21 | return; 22 | } 23 | 24 | processingBeforeExit = true; 25 | 26 | const message = this.getExitCodeReason(code); 27 | if (message) { 28 | void this._client?.submitLog("beforeExit", message, "Error"); 29 | } 30 | 31 | if (this._client?.config.sessionsEnabled) { 32 | void this._client?.submitSessionEnd(); 33 | } 34 | 35 | void this._client?.suspend(); 36 | // Application will now exit. 37 | }); 38 | 39 | return Promise.resolve(); 40 | } 41 | 42 | /** 43 | * exit codes: https://nodejs.org/api/process.html#process_event_exit 44 | * From now on, only synchronous code may run. As soon as this method 45 | * ends, the application inevitably will exit. 46 | */ 47 | private getExitCodeReason(exitCode: number): string | null { 48 | if (exitCode === 1) { 49 | return "Uncaught Fatal Exception"; 50 | } 51 | 52 | if (exitCode === 3) { 53 | return "Internal JavaScript Parse Error"; 54 | } 55 | 56 | if (exitCode === 4) { 57 | return "Internal JavaScript Evaluation Failure"; 58 | } 59 | 60 | if (exitCode === 5) { 61 | return "Fatal Exception"; 62 | } 63 | 64 | if (exitCode === 6) { 65 | return "Non-function Internal Exception Handler "; 66 | } 67 | 68 | if (exitCode === 7) { 69 | return "Internal Exception Handler Run-Time Failure"; 70 | } 71 | 72 | if (exitCode === 8) { 73 | return "Uncaught Exception"; 74 | } 75 | 76 | if (exitCode === 9) { 77 | return "Invalid Argument"; 78 | } 79 | 80 | if (exitCode === 10) { 81 | return "Internal JavaScript Run-Time Failure"; 82 | } 83 | 84 | if (exitCode === 12) { 85 | return "Invalid Debug Argument"; 86 | } 87 | 88 | return null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/node/src/plugins/NodeRequestInfoPlugin.ts: -------------------------------------------------------------------------------- 1 | import { EventPluginContext, getCookies, IEventPlugin, isEmpty, isMatch, KnownEventDataKeys, RequestInfo, stringify } from "@exceptionless/core"; 2 | 3 | export class NodeRequestInfoPlugin implements IEventPlugin { 4 | public priority: number = 70; 5 | public name: string = "NodeRequestInfoPlugin"; 6 | 7 | public run(context: EventPluginContext): Promise { 8 | if (context.event.data && !context.event.data[KnownEventDataKeys.RequestInfo]) { 9 | const requestInfo: RequestInfo | undefined = this.getRequestInfo(context); 10 | if (requestInfo) { 11 | if (isMatch(requestInfo.user_agent, context.client.config.userAgentBotPatterns)) { 12 | context.log.info("Cancelling event as the request user agent matches a known bot pattern"); 13 | context.cancelled = true; 14 | } else { 15 | context.event.data[KnownEventDataKeys.RequestInfo] = requestInfo; 16 | } 17 | } 18 | } 19 | 20 | return Promise.resolve(); 21 | } 22 | 23 | private getRequestInfo(context: EventPluginContext): RequestInfo | undefined { 24 | // TODO: Move this into a known keys. 25 | const REQUEST_KEY: string = "@request"; 26 | if (!context.eventContext[REQUEST_KEY]) { 27 | return; 28 | } 29 | 30 | const config = context.client.config; 31 | const exclusions = config.dataExclusions; 32 | 33 | type requestShape = { 34 | method: string; 35 | secure: boolean; 36 | ip: string; 37 | hostname: string; 38 | path: string; 39 | headers: Record; 40 | params: Record; 41 | body?: object; 42 | }; 43 | 44 | const request = context.eventContext[REQUEST_KEY] as requestShape; 45 | const requestInfo: RequestInfo = { 46 | user_agent: request.headers["user-agent"], 47 | http_method: request.method, 48 | is_secure: request.secure, 49 | host: request.hostname, 50 | path: request.path, 51 | referrer: request.headers.referer 52 | }; 53 | 54 | const host = request.headers.host; 55 | const port: number = (host && parseInt(host.slice(host.indexOf(":") + 1), 10)) || 0; 56 | if (port > 0) { 57 | requestInfo.port = port; 58 | } 59 | 60 | if (config.includeIpAddress) { 61 | requestInfo.client_ip_address = request.ip; 62 | } 63 | 64 | if (config.includeCookies) { 65 | requestInfo.cookies = getCookies(request.headers.cookie, exclusions) as Record; 66 | } 67 | 68 | if (config.includeHeaders) { 69 | const ignoredHeaders = ["Authorization", "Cookie", "Host", "Method", "Path", "Proxy-Authorization", "Referer", "User-Agent"]; 70 | 71 | const json = stringify(request.headers, [...ignoredHeaders, ...exclusions]); 72 | if (!isEmpty(json)) { 73 | const headers: Record = {}; 74 | const parsedHeaders = JSON.parse(json) as Record; 75 | for (const key in parsedHeaders) { 76 | headers[key] = parsedHeaders[key].split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map((value) => value.trim()); 77 | } 78 | 79 | requestInfo.headers = headers; 80 | } 81 | } 82 | 83 | if (config.includeQueryString) { 84 | const json = stringify(request.params, exclusions); 85 | if (!isEmpty(json)) { 86 | requestInfo.query_string = JSON.parse(json) as Record; 87 | } 88 | } 89 | 90 | if (config.includePostData) { 91 | const json = stringify(request.body, exclusions); 92 | if (!isEmpty(json)) { 93 | requestInfo.post_data = JSON.parse(json) as Record; 94 | } 95 | } 96 | 97 | return requestInfo; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/node/src/plugins/NodeWrapFunctions.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionlessClient, IEventPlugin, PluginContext } from "@exceptionless/core"; 2 | 3 | export class NodeWrapFunctions implements IEventPlugin { 4 | public priority: number = 110; 5 | public name: string = "NodeWrapFunctions"; 6 | 7 | private _client: ExceptionlessClient | null = null; 8 | 9 | public startup(context: PluginContext): Promise { 10 | if (this._client) { 11 | return Promise.resolve(); 12 | } 13 | 14 | this._client = context.client; 15 | 16 | // TODO: TraceKit.extendToAsynchronousCallbacks(); 17 | 18 | return Promise.resolve(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/node/src/storage/NodeDirectoryStorage.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "@exceptionless/core"; 2 | 3 | import { mkdirSync } from "fs"; 4 | import { readdir, readFile, unlink, writeFile } from "fs/promises"; 5 | import { dirname, join, resolve } from "path"; 6 | import { argv } from "process"; 7 | 8 | export class NodeDirectoryStorage implements IStorage { 9 | private directory: string; 10 | 11 | constructor(directory?: string) { 12 | if (!directory) { 13 | this.directory = argv && argv.length > 1 ? join(dirname(argv[1]), ".exceptionless") : ".exceptionless"; 14 | } else { 15 | this.directory = resolve(directory); 16 | } 17 | 18 | mkdirSync(this.directory, { recursive: true }); 19 | } 20 | 21 | public async length(): Promise { 22 | const keys = await this.keys(); 23 | return keys.length; 24 | } 25 | 26 | public async clear(): Promise { 27 | for (const key of await this.keys()) { 28 | await this.removeItem(key); 29 | } 30 | 31 | return Promise.resolve(); 32 | } 33 | 34 | public async getItem(key: string): Promise { 35 | try { 36 | return await readFile(join(this.directory, key), "utf8"); 37 | } catch (ex) { 38 | if (this.isNodeError(ex) && ex.code === "ENOENT") { 39 | return null; 40 | } 41 | 42 | throw ex; 43 | } 44 | } 45 | 46 | public async key(index: number): Promise { 47 | const keys = await this.keys(); 48 | return Promise.resolve(index < keys.length ? keys[index] : null); 49 | } 50 | 51 | public async keys(): Promise { 52 | return await readdir(this.directory); 53 | } 54 | 55 | public async removeItem(key: string): Promise { 56 | try { 57 | await unlink(join(this.directory, key)); 58 | } catch (ex) { 59 | if (this.isNodeError(ex) && ex.code !== "ENOENT") { 60 | throw ex; 61 | } 62 | } 63 | } 64 | 65 | public async setItem(key: string, value: string): Promise { 66 | await writeFile(join(this.directory, key), value); 67 | } 68 | 69 | private isNodeError(error: unknown): error is NodeJS.ErrnoException { 70 | return typeof error === "object"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/node/test/storage/NodeDirectoryStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "@exceptionless/core"; 2 | import { describeStorage } from "../../../core/test/storage/StorageTestBase.js"; 3 | import { NodeDirectoryStorage } from "../../src/storage/NodeDirectoryStorage.js"; 4 | import { mkdirSync, rmSync } from "fs"; 5 | 6 | const directory: string = "./test/data"; 7 | 8 | function resetStorageDirectory() { 9 | rmSync(directory, { recursive: true, force: true }); 10 | mkdirSync(directory); 11 | } 12 | 13 | describeStorage("NodeDirectoryStorage", (): IStorage => new NodeDirectoryStorage(directory), resetStorageDirectory, resetStorageDirectory); 14 | -------------------------------------------------------------------------------- /packages/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022"], 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "types": ["node", "jest"] 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # Exceptionless React 2 | 3 | The Exceptionless React package provides a native way to handle errors and events in React. This means errors inside your components, which tend to crash your entire app, can be sent to Exceptionless and you can be alerted. Additionally, you can use this package to catch errors throughout your non-component functions such as in Redux actions, utility functions, etc. 4 | 5 | ## Getting Started 6 | 7 | To use this package, your must be using ES6 and support ESM modules. 8 | 9 | ## Install 10 | 11 | `npm install @exceptionless/react --save` 12 | 13 | ## Configuration 14 | 15 | While your app is starting up, you should call `startup` on the Exceptionless 16 | client. This ensures the client is configured and automatic capturing of 17 | unhandled errors occurs. 18 | 19 | ```jsx 20 | import { Exceptionless, ExceptionlessErrorBoundary } from "@exceptionless/react"; 21 | 22 | class App extends Component { 23 | async componentDidMount() { 24 | await Exceptionless.startup((c) => { 25 | c.apiKey = "API_KEY_HERE"; 26 | c.setUserIdentity("12345678", "Blake"); 27 | 28 | c.defaultTags.push("Example", "React"); 29 | }); 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 |
// YOUR APP COMPONENTS HERE
36 |
37 | ); 38 | } 39 | } 40 | 41 | export default App; 42 | ``` 43 | 44 | ## Handling Events 45 | 46 | While errors within the components themselves are automatically sent to Exceptionless, you will still want to handle events that happen outside the components. 47 | 48 | Because the Exceptionless client is a singleton, it is available anywhere in your app where you import it. Here's an example from a file we'll call `utilities.js`. 49 | 50 | ```js 51 | export const myUtilityFunction = async () => { 52 | try { 53 | // Handle successful run of code 54 | } catch (e) { 55 | // If there's an error, send it to Exceptionless 56 | await Exceptionless.submitException(e); 57 | } 58 | }; 59 | ``` 60 | 61 | You can also sent events and logs that are not errors by simply calling the built-in methods on the Exceptionless class: 62 | 63 | ```js 64 | await Exceptionless.submitLog("Hello, world!"); 65 | await Exceptionless.submitFeatureUsage("New Shopping Cart Feature"); 66 | ``` 67 | 68 | Please see the [docs](https://exceptionless.com/docs/clients/javascript/) for 69 | more information on configuring the client. 70 | 71 | ## Support 72 | 73 | If you need help, please contact us via in-app support, [open an issue](https://github.com/exceptionless/Exceptionless.JavaScript/issues/new) or [join our chat on Discord](https://discord.gg/6HxgFCx). We’re always here to help if you have any questions! 74 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exceptionless/react", 3 | "version": "3.0.0-dev", 4 | "description": "JavaScript client for Exceptionless", 5 | "author": { 6 | "name": "exceptionless", 7 | "url": "https://exceptionless.com" 8 | }, 9 | "keywords": [ 10 | "exceptionless", 11 | "error", 12 | "feature", 13 | "logging", 14 | "tracking", 15 | "reporting", 16 | "react" 17 | ], 18 | "repository": { 19 | "url": "git://github.com/exceptionless/Exceptionless.JavaScript.git", 20 | "type": "git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/exceptionless/Exceptionless.JavaScript/issues" 24 | }, 25 | "license": "Apache-2.0", 26 | "type": "module", 27 | "main": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "unpkg": "dist/index.bundle.min.js", 30 | "jsdelivr": "dist/index.bundle.min.js", 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./package.json": "./package.json" 34 | }, 35 | "scripts": { 36 | "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.min.js", 37 | "watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --watch --outfile=dist/index.bundle.js" 38 | }, 39 | "sideEffects": false, 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "devDependencies": { 44 | "@jest/globals": "^29.7.0", 45 | "@types/react": "^18.2.73", 46 | "@types/react-dom": "^18.2.23", 47 | "esbuild": "^0.20.2", 48 | "jest": "^29.7.0", 49 | "jest-ts-webcompat-resolver": "^1.0.0", 50 | "ts-jest": "^29.1.2" 51 | }, 52 | "dependencies": { 53 | "@exceptionless/browser": "3.0.0-dev" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/react/src/ExceptionlessErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, PropsWithChildren } from "react"; 2 | import { Exceptionless } from "@exceptionless/browser"; 3 | 4 | type ErrorState = { 5 | hasError: boolean; 6 | }; 7 | 8 | export class ExceptionlessErrorBoundary extends Component { 9 | constructor(props: Readonly> | Record) { 10 | super(props); 11 | } 12 | 13 | async componentDidCatch(error: Error, errorInfo: unknown) { 14 | await Exceptionless.createException(error).setProperty("errorInfo", errorInfo).submit(); 15 | } 16 | 17 | render() { 18 | return this.props.children; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@exceptionless/browser"; 2 | 3 | export { ExceptionlessErrorBoundary } from "./ExceptionlessErrorBoundary.js"; 4 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ES2022"], 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "jsx": "react" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | # Exceptionless vue 2 | 3 | The Exceptionless Vue package provides a native way to handle errors and events 4 | in Vue. This means errors inside your components, which tend to crash your 5 | entire app, can be sent to Exceptionless and you can be alerted. Additionally, 6 | you can use this package to catch errors throughout your non-component functions 7 | such as in utility functions, etc. 8 | 9 | ## Getting Started 10 | 11 | To use this package, your must be using ES6 and support ESM modules. 12 | 13 | ## Install 14 | 15 | `npm install @exceptionless/vue --save` 16 | 17 | ## Configuration 18 | 19 | While your app is starting up, you should call `startup` on the Exceptionless 20 | client. This ensures the client is configured and automatic capturing of 21 | unhandled errors occurs. Below is from an example vue applications `main.js` file. 22 | 23 | ```js 24 | import { createApp } from "vue"; 25 | import App from "./App.vue"; 26 | import { Exceptionless, ExceptionlessErrorHandler } from "@exceptionless/vue"; 27 | 28 | Exceptionless.startup((c) => { 29 | c.apiKey = "API_KEY_HERE"; 30 | c.setUserIdentity("12345678", "Blake"); 31 | 32 | c.defaultTags.push("Example", "Vue"); 33 | }); 34 | 35 | const app = createApp(App); 36 | // Set the global vue error handler. 37 | app.config.errorHandler = ExceptionlessErrorHandler; 38 | app.mount("#app"); 39 | ``` 40 | 41 | ## Handling Events 42 | 43 | While errors within the components themselves are automatically sent to 44 | Exceptionless, you will still want to handle events that happen outside the 45 | components. 46 | 47 | Because the Exceptionless client is a singleton, it is available anywhere in 48 | your app where you import it. Here's an example from a file we'll call `utilities.js`. 49 | 50 | ```js 51 | export const myUtilityFunction = async () => { 52 | try { 53 | // Handle successful run of code 54 | } catch (e) { 55 | // If there's an error, send it to Exceptionless 56 | await Exceptionless.submitException(e); 57 | } 58 | }; 59 | ``` 60 | 61 | You can also sent events and logs that are not errors by simply calling the 62 | built-in methods on the Exceptionless class: 63 | 64 | ```js 65 | await Exceptionless.submitLog("Hello, world!"); 66 | await Exceptionless.submitFeatureUsage("New Shopping Cart Feature"); 67 | ``` 68 | 69 | Please see the [docs](https://exceptionless.com/docs/clients/javascript/) for 70 | more information on configuring the client. 71 | 72 | ## Support 73 | 74 | If you need help, please contact us via in-app support, 75 | [open an issue](https://github.com/exceptionless/Exceptionless.JavaScript/issues/new) 76 | or [join our chat on Discord](https://discord.gg/6HxgFCx). We’re always here to 77 | help if you have any questions! 78 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exceptionless/vue", 3 | "version": "3.0.0-dev", 4 | "description": "JavaScript client for Exceptionless", 5 | "author": { 6 | "name": "exceptionless", 7 | "url": "https://exceptionless.com" 8 | }, 9 | "keywords": [ 10 | "exceptionless", 11 | "error", 12 | "feature", 13 | "logging", 14 | "tracking", 15 | "reporting", 16 | "vue" 17 | ], 18 | "repository": { 19 | "url": "git://github.com/exceptionless/Exceptionless.JavaScript.git", 20 | "type": "git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/exceptionless/Exceptionless.JavaScript/issues" 24 | }, 25 | "license": "Apache-2.0", 26 | "type": "module", 27 | "main": "dist/index.js", 28 | "types": "dist/index.d.ts", 29 | "unpkg": "dist/index.bundle.min.js", 30 | "jsdelivr": "dist/index.bundle.min.js", 31 | "exports": { 32 | ".": "./dist/index.js", 33 | "./package.json": "./package.json" 34 | }, 35 | "scripts": { 36 | "build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2022 --format=esm --outfile=dist/index.bundle.min.js", 37 | "watch": "tsc -p tsconfig.json -w --preserveWatchOutput & && esbuild src/index.ts --bundle --sourcemap --target=es2022 --format=esm --watch --outfile=dist/index.bundle.js &" 38 | }, 39 | "sideEffects": false, 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "devDependencies": { 44 | "esbuild": "^0.20.2" 45 | }, 46 | "dependencies": { 47 | "@exceptionless/browser": "3.0.0-dev" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@exceptionless/browser"; 2 | import { Exceptionless } from "@exceptionless/browser"; 3 | 4 | /** 5 | * https://vuejs.org/v2/api/#errorHandler 6 | * https://v3.vuejs.org/api/application-config.html#errorhandler 7 | * @param err 8 | * @param vm 9 | * @param info 10 | */ 11 | export const ExceptionlessErrorHandler = async (err: Error, vm: unknown, info: unknown): Promise => { 12 | await Exceptionless.createException(err).setProperty("vm", vm).setProperty("info", info).submit(); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ES2022"], 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "esModuleInterop": true, 6 | "exactOptionalPropertyTypes": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "isolatedModules": true, 9 | "lib": ["DOM", "ES2022"], 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "pretty": true, 18 | "skipLibCheck": true, 19 | "sourceMap": true, 20 | "strict": true, 21 | "target": "ES2022", 22 | "useUnknownInCatchVariables": true 23 | } 24 | } 25 | --------------------------------------------------------------------------------