├── .DS_Store ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── build-test.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── LICENSE ├── README.md ├── cartridges ├── .DS_Store ├── lib_sentry │ ├── .project │ ├── caches.json │ ├── cartridge │ │ ├── config │ │ │ └── sentry.json │ │ ├── lib_sentry.properties │ │ ├── models │ │ │ ├── SentryBreadcrumb.js │ │ │ ├── SentryEvent.js │ │ │ ├── SentryId.js │ │ │ ├── SentryOptions.js │ │ │ ├── SentryRequest.js │ │ │ └── SentryUser.js │ │ └── scripts │ │ │ ├── Sentry.js │ │ │ ├── helpers │ │ │ └── sentryHelper.js │ │ │ ├── processors │ │ │ ├── basketProcessor.js │ │ │ ├── cookieProcessor.js │ │ │ └── duplicateEventProcessor.js │ │ │ └── services │ │ │ └── sentryService.js │ └── package.json └── plugin_sentry │ ├── .project │ ├── cartridge │ ├── client │ │ └── default │ │ │ └── js │ │ │ ├── .eslintrc.json │ │ │ ├── sentry.js │ │ │ └── sentry │ │ │ └── main.js │ ├── controllers │ │ └── Error.js │ ├── plugin_sentry.properties │ ├── scripts │ │ ├── hooks.json │ │ └── hooks │ │ │ └── sentry │ │ │ └── htmlHead.js │ ├── static │ │ └── default │ │ │ └── js │ │ │ └── sentry.js │ └── templates │ │ └── default │ │ └── sentry │ │ └── tag.isml │ └── package.json ├── ismllinter.config.js ├── metadata ├── services.xml └── site-preferences.xml ├── package.json ├── test └── unit │ ├── .eslintrc.json │ └── lib_sentry │ ├── models │ ├── SentryBreadcrumb.js │ ├── SentryEvent.js │ ├── SentryId.js │ ├── SentryOptions.js │ ├── SentryRequest.js │ └── SentryUser.js │ └── scripts │ ├── processors │ ├── basketProcessor.js │ ├── cookieProcessor.js │ └── duplicateEventProcessor.js │ ├── sentry.js │ └── services │ └── sentryService.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taurgis/link_sentry/6c625339e35caf742e2c4913b29c550d4cbfc910/.DS_Store -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | cartridges/plugin_sentry/cartridge/static/ 2 | coverage/ 3 | doc/ 4 | bin/ 5 | codecept.conf.js 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base/legacy", 4 | "parserOptions": { 5 | "ecmaVersion": 6 6 | }, 7 | "rules": { 8 | "import/no-unresolved": "off", 9 | "indent": ["error", 4, { "SwitchCase": 1, "VariableDeclarator": 1 }], 10 | "func-names": "off", 11 | "require-jsdoc": "error", 12 | "valid-jsdoc": ["error", { "preferType": { "Boolean": "boolean", "Number": "number", "object": "Object", "String": "string" }, "requireReturn": false}], 13 | "vars-on-top": "off", 14 | "global-require": "off", 15 | "no-shadow": ["error", { "allow": ["err", "callback"]}], 16 | "max-len": "off" 17 | }, 18 | "globals": { 19 | "request": true, 20 | "empty": true, 21 | "session": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - master 7 | pull_request: 8 | branches: 9 | - develop 10 | - master 11 | jobs: 12 | build_and_test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 16.x] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm run compile:js 25 | - run: npm run lint 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dw.json 2 | connections-source.json 3 | .idea/ 4 | node_modules/ 5 | !cartridges/plugin_filternavigation/**/static/**/js 6 | 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run test 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thomas Theunen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # link_sentry: Storefront Reference Architecture (SFRA) 2 | 3 | This is the repository for the link_sentry plugin. This plugin adds Sentry Monitoring, including 4 | the following capabilities: 5 | 6 | * Client Side error reporting 7 | * Server-Side error reporting 8 | * Optional library functions to log custom errors to sentry 9 | 10 | # Cartridge Path Considerations 11 | 12 | The link_sentry plugin requires the app\_storefront\_base cartridge. In your cartridge path, include the cartridges in 13 | the following order: 14 | 15 | ``` 16 | plugin_sentry:lib_sentry:app_storefront_base 17 | ``` 18 | 19 | # Getting Started 20 | 21 | 1. Clone this repository. (The name of the top-level folder is link_sentry.) 22 | 2. In the top-level plugin_gtm folder, enter the following command: `npm install`. (This command installs all of the 23 | package dependencies required for this plugin.) 24 | 3. In the top-level link_sentry folder, edit the paths.base property in the package.json file. This property should 25 | contain a relative path to the local directory containing the Storefront Reference Architecture repository. For 26 | example: 27 | 28 | ``` 29 | "paths": { 30 | "base": "../storefront-reference-architecture/cartridges/app_storefront_base/" 31 | } 32 | ``` 33 | 34 | 4. In the top-level link_sentry folder, enter the following command: `npm run compile:js` 35 | 5. In the top-level link_sentry folder, enter the following command: `npm run uploadCartridge` 36 | 37 | For information on Getting Started with SFRA, 38 | see [Get Started with SFRA](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2Fcontent%2Fb2c_commerce%2Ftopics%2Fsfra%2Fb2c_sfra_setup.html) 39 | . 40 | 41 | #Metadata 42 | 43 | ## Services 44 | To contact Sentry it is required to configure a service (without credentials). You can import a file containing the required configuration in the business manager. The file is located here `metadata/services.xml`. 45 | 46 | Other than importing these services, no configuration is required. 47 | 48 | ## Site Preferences 49 | A few preferences are added to the `Site Preferences` system object. The file `metadata/site-preferences.xml` can be 50 | imported as a System Object Type in the business manager. 51 | 52 | ### Configuration of the Site Preferences 53 | 54 | #### Public Key (DSN) 55 | This value can be found in your project configuration in Sentry at: 56 | 57 | `Project > SDK SETUP > Client Keys (DSN)` 58 | 59 | #### Project ID (Name) 60 | This value can be found in your project configuration in Sentry at: 61 | 62 | `Project > General Settings` 63 | 64 | #### Cookie Tracking Enabled 65 | You can enable or disable the adding of cookies to the Sentry Events sent by the server-side code. 66 | 67 | # Usage 68 | 69 | ## Configuration file 70 | There is a configuration file located at `lib_sentry/config/sentry.json`. 71 | 72 | Example: 73 | ``` 74 | { 75 | "code-version": "1.0.0", // This needs to be your unique identifier for releases in Sentry 76 | "sentry-client": { //SDK Information, this can remain the same always 77 | "name": "SFRA", 78 | "version": "5.3.0" 79 | } 80 | } 81 | ``` 82 | To make sure bugs can be related to releases in Sentry, make sure you keep the `code-version` in this file in sync with your releases. 83 | 84 | ## Initialization 85 | 86 | Using the default configuration (Site Preferences) 87 | 88 | ``` 89 | var Sentry = require('*/cartridge/scripts/Sentry'); 90 | Sentry.captureMessage('Hello, world!'); 91 | Sentry.captureException(new Error('Good bye')); 92 | ``` 93 | 94 | Using your own configuration, ignoring Site Preferences 95 | 96 | ``` 97 | var Sentry = require('*\/cartridge/scripts/Sentry'); 98 | 99 | Sentry.init({ 100 | dsn: '__DSN__', 101 | // ... 102 | }); 103 | 104 | Sentry.captureException(new Error('Good bye')); 105 | ``` 106 | 107 | ## Hooks 108 | 109 | ### com.sentry.beforesend 110 | Use this hook to prevent certain events from being sent to Sentry, or to make modifications to it. 111 | ``` 112 | Hook: com.sentry.beforesend 113 | Function: beforeSend 114 | Parameters: SentryEvent object 115 | 116 | Example: 117 | 118 | function beforeSend(sentryEvent) { 119 | // your logic, if this function returns null, the event is not sent. 120 | 121 | return sentryEvent; 122 | } 123 | 124 | module.exports = { 125 | beforeSend: beforeSend 126 | }; 127 | ``` 128 | 129 | Never used hooks before? Look at the documentation [here](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/sfra/b2c_sfra_hooks.html?resultof=%22%68%6f%6f%6b%73%22%20%22%68%6f%6f%6b%22%20). 130 | 131 | ## Custom Processors 132 | It is possible to hook in your own custom processors (event extenders) without having to overwrite the entire 133 | codebase. e.g. you could create a generic helper function to add some default processors 134 | of your own. 135 | 136 | 137 | ``` 138 | var BasketProcessor = require('*/cartridge/scripts/processors/basketProcessor'); 139 | var Sentry = require('*/cartridge/scripts/Sentry'); 140 | Sentry.getOptions().addEventProcessor(BasketProcessor); 141 | 142 | Sentry.captureException(new Error('Good bye')); 143 | ``` 144 | 145 | # Release management 146 | 147 | # NPM scripts 148 | 149 | Use the provided NPM scripts to compile and upload changes to your sandbox. 150 | 151 | ## Compiling your application 152 | 153 | * `npm run compile:js` - Compiles all js files and aggregates them. 154 | 155 | **Note:** The plugin cartridge must be compiled after compiling storefront-reference-architecture (SFRA base) cartridge. 156 | 157 | ## Linting your code 158 | 159 | `npm run lint` - Execute linting for all JavaScript and SCSS files in the project. 160 | 161 | ## Watching for changes and uploading 162 | 163 | `npm run watch` - Watches everything and recompiles (if necessary) and uploads to the sandbox. Requires a valid dw.json 164 | file at the root that is configured for the sandbox to upload. 165 | -------------------------------------------------------------------------------- /cartridges/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taurgis/link_sentry/6c625339e35caf742e2c4913b29c550d4cbfc910/cartridges/.DS_Store -------------------------------------------------------------------------------- /cartridges/lib_sentry/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | lib_sentry 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/caches.json: -------------------------------------------------------------------------------- 1 | { 2 | "caches": [ 3 | { 4 | "id": "sentryConfig", 5 | "expireAfterSeconds": 3600 6 | }, 7 | { 8 | "id": "sentryEvents", 9 | "expireAfterSeconds": 3600 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/config/sentry.json: -------------------------------------------------------------------------------- 1 | { 2 | "code-version": "6.2.0", 3 | "sentry-client": { 4 | "name": "SFRA", 5 | "version": "6.2.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/lib_sentry.properties: -------------------------------------------------------------------------------- 1 | # cartridge.properties for cartridge lib_sentry 2 | # Tue Feb 23 08:01:09 CET 2021 3 | demandware.cartridges.lib_sentry.multipleLanguageStorefront=true 4 | demandware.cartridges.lib_sentry.id=lib_sentry 5 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/models/SentryBreadcrumb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { forEach } = require('*/cartridge/scripts/util/collections'); 4 | 5 | /** 6 | * Series of application events based on the user who triggered the event. 7 | * 8 | * @param {dw.system.Request} request - The HTTP request 9 | * @constructor 10 | */ 11 | function SentryBreadcrumb(request) { 12 | if (!request || !request.session) { 13 | return null; 14 | } 15 | 16 | var clickStream = request.session.clickStream; 17 | var values = []; 18 | 19 | if (clickStream.enabled) { 20 | var previousClick; 21 | 22 | forEach(clickStream.clicks, function (click) { 23 | values.push({ 24 | timestamp: Math.round(click.timestamp / 1000), 25 | type: 'navigation', 26 | data: { 27 | from: previousClick ? previousClick.url : null, 28 | to: click.url 29 | } 30 | }); 31 | 32 | previousClick = click; 33 | }); 34 | } 35 | 36 | this.values = values; 37 | } 38 | 39 | module.exports = SentryBreadcrumb; 40 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/models/SentryEvent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var VALID_LEVELS = ['fatal', 4 | 'error', 5 | 'warning', 6 | 'info', 7 | 'debug']; 8 | 9 | var ENVIRONMENT_MAPPING = { 10 | 0: 'development', 11 | 1: 'staging', 12 | 2: 'production' 13 | }; 14 | 15 | /** 16 | * The model representing a Sentry Exception to be posted to the Sentry API. 17 | * 18 | * @param {Object} data - The event data 19 | * @param {number} data.eventType - The event type 20 | * @param {string} data.level - The error severity. 21 | * @param {Error} data.error - The error 22 | * @param {string} data.type - The error type 23 | * @param {string} data.release - The release version 24 | * 25 | * @constructor 26 | */ 27 | function SentryEvent(data) { 28 | // If any of these are missing, don't try to create a Sentry Event object 29 | if (!data || !data.error || !data.release) { 30 | return; 31 | } 32 | 33 | var { getInstanceType } = require('dw/system/System'); 34 | var SentryId = require('*/cartridge/models/SentryId'); 35 | var SentryUser = require('*/cartridge/models/SentryUser'); 36 | var SentryRequest = require('*/cartridge/models/SentryRequest'); 37 | var SentryBreadcrumb = require('*/cartridge/models/SentryBreadcrumb'); 38 | 39 | Object.defineProperty(this, 'error', { 40 | value: data.error, 41 | enumerable: false 42 | }); 43 | 44 | this.event_id = new SentryId(null).toString(); 45 | this.timestamp = Math.round(Date.now() / 1000); 46 | this.platform = 'javascript'; 47 | this.transaction = request.httpPath; 48 | this.server_name = request.httpHost; 49 | this.release = data.release; 50 | this.environment = ENVIRONMENT_MAPPING[getInstanceType().valueOf()]; 51 | 52 | if (data.level && VALID_LEVELS.indexOf(data.level) >= 0) { 53 | this.level = data.level; 54 | } else { 55 | this.level = SentryEvent.LEVEL_FATAL; 56 | } 57 | 58 | if (data.eventType === SentryEvent.TYPE_EXCEPTION) { 59 | this.exception = { 60 | values: [{ 61 | type: this.error.constructor.name, 62 | value: this.error.message + (this.error.stack ? '\n' + this.error.stack : '') 63 | }] 64 | }; 65 | } else { 66 | this.message = { 67 | message: data.message 68 | }; 69 | } 70 | 71 | this.user = new SentryUser(request.httpRemoteAddress, request.session.customer); 72 | this.request = new SentryRequest(request); 73 | this.breadcrumbs = new SentryBreadcrumb(request); 74 | } 75 | 76 | SentryEvent.LEVEL_FATAL = 'fatal'; 77 | SentryEvent.LEVEL_ERROR = 'error'; 78 | SentryEvent.LEVEL_WARNING = 'warning'; 79 | SentryEvent.LEVEL_INFO = 'info'; 80 | SentryEvent.LEVEL_DEBUG = 'debug'; 81 | SentryEvent.ERROR_TYPE_UNKNOWN = 'unknown_error'; 82 | SentryEvent.ERROR_TYPE_SYNTAX_ERROR = 'SyntaxError'; 83 | SentryEvent.ERROR_TYPE_TYPE_SECURITY_VIOLATION = 'security_violation'; 84 | SentryEvent.TYPE_EXCEPTION = 0; 85 | SentryEvent.TYPE_MESSAGE = 1; 86 | 87 | module.exports = SentryEvent; 88 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/models/SentryId.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { createUUID } = require('dw/util/UUIDUtils'); 4 | 5 | /** 6 | * Generates a Sentry UUID according to specifications 7 | * @return {string} - The generated UUID 8 | */ 9 | function generateSentryUUID() { 10 | return (createUUID() + createUUID()).substring(0, 32).toLowerCase(); 11 | } 12 | 13 | /** 14 | * Model representing a unique SentryId. 15 | * @param {string|null} uuid - The preferred UUID for a Sentry ID 16 | * @constructor 17 | */ 18 | function SentryId(uuid) { 19 | if (uuid && uuid.length !== 32) { 20 | throw new Error('UUID needs to be 32 characters long. Was given the value ' + uuid); 21 | } 22 | 23 | this.uuid = uuid || generateSentryUUID(); 24 | } 25 | 26 | /** 27 | * Returns the SentryId as a String 28 | * 29 | * @return {*|string} - The SentryId 30 | */ 31 | SentryId.prototype.toString = function () { 32 | return this.uuid; 33 | }; 34 | 35 | module.exports = SentryId; 36 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/models/SentryOptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DuplicateEventProcessor = require('*/cartridge/scripts/processors/duplicateEventProcessor'); 4 | var CookieProcessor = require('*/cartridge/scripts/processors/cookieProcessor'); 5 | 6 | /** 7 | * Sentry SDK options 8 | * 9 | * @param {Object} config - The configuration 10 | * @constructor 11 | */ 12 | function SentryOptions(config) { 13 | this.dsn = config.dsn; 14 | this.release = config.release; 15 | this.eventProcessors = [new DuplicateEventProcessor(this), new CookieProcessor(this)]; 16 | this.logger = require('dw/system/Logger').getLogger('sentry'); 17 | } 18 | 19 | /** 20 | * Adds an event processor 21 | * 22 | * @param {Object} EventProcessor - The event processor 23 | */ 24 | SentryOptions.prototype.addEventProcessor = function (EventProcessor) { 25 | this.eventProcessors.push(new EventProcessor(this)); 26 | }; 27 | 28 | /** 29 | * Returns the list of event processors 30 | * 31 | * @return {[function]} the event processor list 32 | */ 33 | SentryOptions.prototype.getEventProcessors = function () { 34 | return this.eventProcessors; 35 | }; 36 | 37 | /** 38 | * Returns the configured Logger. 39 | * 40 | * @return {dw.system.Log} - the logger 41 | */ 42 | SentryOptions.prototype.getLogger = function () { 43 | return this.logger; 44 | }; 45 | 46 | /** 47 | * Sets the Logger 48 | * 49 | * @param {dw.system.Log|dw.system.Logger} logger the logger interface 50 | */ 51 | SentryOptions.prototype.setLogger = function (logger) { 52 | if (!logger || (typeof logger.debug !== 'function')) { 53 | return; 54 | } 55 | 56 | this.logger = logger; 57 | }; 58 | 59 | module.exports = SentryOptions; 60 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/models/SentryRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { forEach } = require('*/cartridge/scripts/util/collections'); 4 | 5 | /** 6 | * Gets the headers from the HTTP request 7 | * 8 | * @param {dw.system.Request} request - The HTTP request 9 | * @return {Object} - The headers as an object (key - value) 10 | */ 11 | function getHeaders(request) { 12 | if (!request) { 13 | return null; 14 | } 15 | 16 | var headers = {}; 17 | 18 | if (request.httpHeaders) { 19 | forEach(request.httpHeaders.keySet(), function (httpHeader) { 20 | // Filter out the cookies, they should only be included if cookies are allowed. 21 | if (httpHeader !== 'cookie') { 22 | headers[httpHeader] = request.httpHeaders.get(httpHeader); 23 | } 24 | }); 25 | } 26 | return headers; 27 | } 28 | 29 | /** 30 | * Http request information. 31 | * 32 | * The Request interface contains information on a HTTP request related to the event. 33 | * 34 | * @param {dw.system.Request} request - The HTTP request 35 | * @constructor 36 | */ 37 | function SentryRequest(request) { 38 | if (!request) { 39 | return null; 40 | } 41 | 42 | this.method = request.httpMethod; 43 | this.url = request.httpURL.toString(); 44 | this.query_string = request.httpQueryString; 45 | this.env = { 46 | REMOTE_ADDR: request.httpRemoteAddress 47 | }; 48 | 49 | this.headers = getHeaders(request); 50 | } 51 | 52 | module.exports = SentryRequest; 53 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/models/SentryUser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { map } = require('*/cartridge/scripts/util/collections'); 4 | 5 | /** 6 | * Information about the user who triggered an event. 7 | * 8 | * @param {string} ipAddress - The IP Address of the customer 9 | * @param {dw.customer.Customer} customer - The customer object 10 | * @constructor 11 | */ 12 | function SentryUser(ipAddress, customer) { 13 | this.ip_address = ipAddress; 14 | 15 | if (customer) { 16 | this.customer_groups = map(customer.customerGroups, function (customerGroup) { 17 | return customerGroup.ID; 18 | }).join(', '); 19 | 20 | if (customer.authenticated) { 21 | var profile = customer.profile; 22 | 23 | if (profile) { 24 | this.id = profile.customerNo; 25 | } 26 | } 27 | } 28 | } 29 | 30 | module.exports = SentryUser; 31 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/scripts/Sentry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SentryEvent = require('*/cartridge/models/SentryEvent'); 4 | var SentryOptions = require('*/cartridge/models/SentryOptions'); 5 | var { 6 | sendEvent, getLastEventID, getDSN, getProjectName 7 | } = require('*/cartridge/scripts/helpers/sentryHelper'); 8 | var SentryConfig = require('*/cartridge/config/sentry'); 9 | 10 | var BEFORE_SEND_HOOK = 'com.sentry.beforesend'; 11 | var DEFAULT_OPTIONS = new SentryOptions({ 12 | dsn: getDSN(), 13 | release: getProjectName() + '@' + SentryConfig['code-version'] 14 | }); 15 | 16 | /** 17 | * Processes and preprocesses the SentryEvent before it is sent to Sentry 18 | * @param {SentryEvent} sentryEvent - The Sentry Event 19 | * 20 | * @return {SentryId|null} - The Sentry Event ID 21 | */ 22 | function processEventCall(sentryEvent) { 23 | var currentSentryEvent = sentryEvent; 24 | 25 | this.options.getEventProcessors() 26 | .forEach(function (eventProcessor) { 27 | if (currentSentryEvent) { 28 | currentSentryEvent = eventProcessor.process(currentSentryEvent); 29 | } 30 | }); 31 | 32 | if (currentSentryEvent) { 33 | var { hasHook, callHook } = require('dw/system/HookMgr'); 34 | 35 | if (hasHook(BEFORE_SEND_HOOK)) { 36 | currentSentryEvent = callHook(BEFORE_SEND_HOOK, 'beforeSend', currentSentryEvent); 37 | 38 | if (!currentSentryEvent) { 39 | return null; 40 | } 41 | } 42 | 43 | return sendEvent(currentSentryEvent, this.options.dsn); 44 | } 45 | 46 | return null; 47 | } 48 | 49 | /** 50 | * The Sentry SFCC SDK Client. 51 | * 52 | * To use this SDK, call the {@link init} function as early as possible in the 53 | * main entry module. To set context information or send manual events, use the 54 | * provided methods. 55 | * 56 | * @example 57 | * ``` 58 | * 59 | * var Sentry = require('*\/cartridge/scripts/Sentry'); 60 | * 61 | * Sentry.init(); 62 | * Sentry.captureException(new Error('Good bye')); 63 | * ``` 64 | * 65 | * 66 | * @example 67 | * ``` 68 | * var Sentry = require('*\/cartridge/scripts/Sentry'); 69 | * 70 | * Sentry.init({ 71 | * dsn: '__DSN__', 72 | * // ... 73 | * }); 74 | * 75 | * Sentry.captureException(new Error('Good bye')); 76 | * ``` 77 | * 78 | * @example 79 | * ``` 80 | * var Sentry = require('*\/cartridge/scripts/Sentry'); 81 | * Sentry.captureMessage('Hello, world!'); 82 | * Sentry.captureException(new Error('Good bye')); 83 | * Sentry.captureEvent(new SentryEvent({ 84 | * message: 'Manual', 85 | * // ... 86 | * }); 87 | * ``` 88 | */ 89 | function Sentry() { 90 | this.initialized = false; 91 | } 92 | 93 | /** 94 | * Initializes Sentry with the given options. If no options are passed the default 95 | * options will be used. 96 | * 97 | * @param {SentryOptions|null|undefined} sentryOptions - The Sentry Options 98 | * @return {Sentry} - Sentry instance 99 | */ 100 | Sentry.prototype.init = function (sentryOptions) { 101 | this.options = sentryOptions ? new SentryOptions(sentryOptions) : DEFAULT_OPTIONS; 102 | this.initialized = true; 103 | 104 | return this; 105 | }; 106 | 107 | /** 108 | * Returns the options of the Sentry instance. If not initialized, the default options will be used. 109 | * 110 | * @return {SentryOptions} - The current options 111 | */ 112 | Sentry.prototype.getOptions = function () { 113 | if (!this.initialized) { 114 | this.init(DEFAULT_OPTIONS); 115 | } 116 | 117 | return this.options; 118 | }; 119 | 120 | /** 121 | * Captures the message. 122 | * 123 | * @param {string} message - The message to send. 124 | * @return {SentryId} - The Id (SentryId object) of the event 125 | */ 126 | Sentry.prototype.captureMessage = function (message) { 127 | if (!this.initialized) { 128 | this.init(DEFAULT_OPTIONS); 129 | } 130 | 131 | var sentryEvent = new SentryEvent({ 132 | eventType: SentryEvent.TYPE_MESSAGE, 133 | release: this.options.release, 134 | message: message, 135 | level: SentryEvent.LEVEL_INFO 136 | }); 137 | 138 | return sendEvent(sentryEvent, this.options.dsn); 139 | }; 140 | 141 | /** 142 | * Captures an exception event and sends it to Sentry. 143 | * 144 | * @param {Error} error - An exception-like object. 145 | * @returns {SentryId|null} - The event id 146 | */ 147 | Sentry.prototype.captureException = function (error) { 148 | if (!this.initialized) { 149 | this.init(DEFAULT_OPTIONS); 150 | } 151 | 152 | var sentryEvent = new SentryEvent({ 153 | error: error, 154 | eventType: SentryEvent.TYPE_EXCEPTION, 155 | release: this.options.release, 156 | level: SentryEvent.LEVEL_ERROR 157 | }); 158 | 159 | return processEventCall.call(this, sentryEvent); 160 | }; 161 | 162 | /** 163 | * Captures the event. 164 | * 165 | * @param {SentryEvent} sentryEvent - the event 166 | * @returns {SentryId} - The Id (SentryId object) of the event 167 | */ 168 | Sentry.prototype.captureEvent = function (sentryEvent) { 169 | if (!this.initialized) { 170 | this.init(DEFAULT_OPTIONS); 171 | } 172 | 173 | return processEventCall.call(this, sentryEvent); 174 | }; 175 | 176 | /** 177 | * Fetch the last Event ID returned by Sentry. 178 | * 179 | * @returns {SentryId} - The last know Event ID returned by Sentry. 180 | */ 181 | Sentry.prototype.getLastEventID = function () { 182 | var lastEventId = getLastEventID(); 183 | 184 | if (lastEventId) { 185 | return lastEventId; 186 | } 187 | 188 | return null; 189 | }; 190 | 191 | module.exports = new Sentry(); 192 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/scripts/helpers/sentryHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Logger = require('dw/system/Logger').getLogger('sentry'); 4 | var SentryId = require('*/cartridge/models/SentryId'); 5 | 6 | var DSN_PREFERENCE = 'sentryDSN'; 7 | var PROJECT_NAME_PREFERENCE = 'sentryProjectID'; 8 | var COOKIES_PREFERENCE = 'sentryCookiesEnabled'; 9 | 10 | /** 11 | * Fetches a Site Preference, if it has been cached fetch it from the cache. 12 | * @param {string} key - The key of the Site Preference 13 | * @return {Object} - The Site Preference 14 | */ 15 | function getCachedPreference(key) { 16 | var configCache = require('dw/system/CacheMgr').getCache('sentryConfig'); 17 | 18 | Logger.debug('Sentry :: Fetching {0}.', key); 19 | 20 | return configCache.get(key, function () { 21 | var currentSite = require('dw/system/Site').getCurrent(); 22 | 23 | Logger.debug('Sentry :: Fetching {0} from the Site Preference.', key); 24 | 25 | return currentSite.getCustomPreferenceValue(key); 26 | }); 27 | } 28 | 29 | /** 30 | * Stores a Sentry ID in the cache. 31 | * @param {SentryId} sentryId - The Sentry ID 32 | */ 33 | function storeEventID(sentryId) { 34 | if (!sentryId) { 35 | return; 36 | } 37 | 38 | var configCache = require('dw/system/CacheMgr').getCache('sentryConfig'); 39 | var key = request.session.sessionID + 'lastEventID'; 40 | Logger.debug('Sentry :: Storing last Event ID in cache under key {0}.', key); 41 | 42 | configCache.put(key, sentryId.uuid); 43 | } 44 | 45 | /** 46 | * Gets the last known event ID from the cache. 47 | * @return {SentryId} - The Sentry ID 48 | */ 49 | function getLastEventID() { 50 | var configCache = require('dw/system/CacheMgr').getCache('sentryConfig'); 51 | var key = request.session.sessionID + 'lastEventID'; 52 | 53 | Logger.debug('Sentry :: Fetching last Event ID in cache under key {0}.', key); 54 | 55 | /** 56 | * @type {string} 57 | */ 58 | var result = configCache.get(request.session.sessionID + 'lastEventID'); 59 | 60 | if (result) { 61 | return new SentryId(result); 62 | } 63 | 64 | return null; 65 | } 66 | 67 | /** 68 | * Gets the Public Key (DSN) URL from the Site Preference. First try to fetch it from 69 | * the cache, if it is not in there get it from the preference and store it. 70 | * 71 | * @return {string|Object} - The DSN URL 72 | */ 73 | function getDSN() { 74 | return getCachedPreference(DSN_PREFERENCE); 75 | } 76 | 77 | /** 78 | * Gets wether or not it is allowed to send cookie data to Sentry. 79 | * 80 | * @return {string|Object} - The DSN URL 81 | */ 82 | function getCookiesAllowed() { 83 | return getCachedPreference(COOKIES_PREFERENCE); 84 | } 85 | 86 | /** 87 | * Gets the project ID for Sentry. First try to fetch it from 88 | * the cache, if it is not in there get it from the preference and store it. 89 | * 90 | * @return {string|Object} - The project ID 91 | */ 92 | function getProjectName() { 93 | return getCachedPreference(PROJECT_NAME_PREFERENCE); 94 | } 95 | 96 | /** 97 | * Set the amount of seconds we should stop trying to send events to Sentry. 98 | * 99 | * Note: This is a helper function to manage the Retry-After header. 100 | * 101 | * @param {number} seconds - The amount of seconds to stop sending events 102 | */ 103 | function blockSendingEvents(seconds) { 104 | var configCache = require('dw/system/CacheMgr').getCache('sentryConfig'); 105 | var currentDateTime = new Date(); 106 | 107 | currentDateTime.setSeconds(currentDateTime.getSeconds() + seconds); 108 | 109 | Logger.debug('Sentry :: We will stop sending requests until {0}.', currentDateTime); 110 | 111 | configCache.put('retryAfter', currentDateTime.getTime()); 112 | } 113 | 114 | /** 115 | * Checks if we are allowed to send a request to Sentry. 116 | * 117 | * @return {boolean} - Wether or not we can send an event 118 | */ 119 | function canSendEvent() { 120 | var configCache = require('dw/system/CacheMgr').getCache('sentryConfig'); 121 | var retryAfter = configCache.get('retryAfter'); 122 | 123 | Logger.debug('Sentry :: Checking if we can send events to Sentry.'); 124 | 125 | if (retryAfter) { 126 | var currentDateTime = Date.now(); 127 | 128 | Logger.debug( 129 | 'Sentry :: Recently had a Retry-After header, which was set until {0}. Current Time is {1}, so result is {2}.', 130 | new Date(retryAfter), 131 | new Date(currentDateTime), 132 | currentDateTime > retryAfter 133 | ); 134 | 135 | return currentDateTime > retryAfter; 136 | } 137 | 138 | return true; 139 | } 140 | 141 | /** 142 | * Logs an exception in sentry 143 | * @param {Object} sentryEvent - The Sentry Event to send 144 | * @param {string} dsn - The DSN to use 145 | * 146 | * @returns {SentryId} - The Sentry Event ID 147 | */ 148 | function sendEvent(sentryEvent, dsn) { 149 | if (!empty(sentryEvent) && canSendEvent()) { 150 | var sentryService = require('*/cartridge/scripts/services/sentryService'); 151 | 152 | var sentryServiceRequest = sentryService.sentryEvent(sentryEvent, dsn); 153 | var result = sentryServiceRequest.call(); 154 | 155 | if (!result.error) { 156 | storeEventID(result.object); 157 | return result.object; 158 | } 159 | 160 | var resultClient = sentryServiceRequest.getClient(); 161 | 162 | Logger.debug('Sentry :: Sentry returned an error: {0}.', resultClient.statusCode); 163 | 164 | if (resultClient.statusCode === 429) { 165 | Logger.debug('Sentry :: Sentry is under maintenance, or our DSN has reached its monthly limit.'); 166 | 167 | var retryHeaderValues = resultClient.getResponseHeader('Retry-After'); 168 | 169 | if (retryHeaderValues && retryHeaderValues.length > 0) { 170 | var retryHeaderValue = retryHeaderValues.get(0); 171 | 172 | if (retryHeaderValue) { 173 | Logger.debug('Sentry :: Sentry is sending Retry-After with a value of {0}.', retryHeaderValue); 174 | 175 | blockSendingEvents(Number(retryHeaderValue)); 176 | } 177 | } else { 178 | // No header was set, so lets stop for 180 seconds 179 | blockSendingEvents(Number(180)); 180 | } 181 | 182 | Logger.debug('Sentry :: Sentry received error {0}.', result.errorMessage); 183 | 184 | return null; 185 | } 186 | } 187 | 188 | return null; 189 | } 190 | 191 | module.exports = { 192 | getDSN: getDSN, 193 | getProjectName: getProjectName, 194 | getCookiesAllowed: getCookiesAllowed, 195 | sendEvent: sendEvent, 196 | getLastEventID: getLastEventID 197 | }; 198 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/scripts/processors/basketProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | var SentryOptions = require('*/cartridge/models/SentryOptions'); 5 | // eslint-disable-next-line no-unused-vars 6 | var SentryEvent = require('*/cartridge/models/SentryEvent'); 7 | 8 | /** 9 | * Processor to add basket information to the user information. 10 | * 11 | * Note: This processor servers as an example on how to dynamically extend the event. 12 | * 13 | * @param {SentryOptions} options - The options 14 | * @constructor 15 | */ 16 | function BasketProcessor(options) { 17 | if (!options) { 18 | throw new Error('The sentryOptions is required.'); 19 | } 20 | 21 | this.options = options; 22 | } 23 | 24 | /** 25 | * Processes the event. 26 | * 27 | * @param {SentryEvent} sentryEvent - The event to process 28 | * 29 | * @returns {SentryEvent} - The processed event 30 | */ 31 | BasketProcessor.prototype.process = function (sentryEvent) { 32 | var { getCurrentBasket } = require('dw/order/BasketMgr'); 33 | var currentBasket = getCurrentBasket(); 34 | var currentSentryEvent = sentryEvent; 35 | 36 | if (currentBasket && currentSentryEvent && currentSentryEvent.user) { 37 | var { map } = require('*/cartridge/scripts/util/collections'); 38 | 39 | this.options.logger.debug('Sentry :: Setting basket information on event.'); 40 | 41 | currentSentryEvent.user.basket_products = map(currentBasket.productLineItems, function (productLineItem) { 42 | return productLineItem.productName + ' (' + productLineItem.productID + ')'; 43 | }).join(', '); 44 | } 45 | 46 | return currentSentryEvent; 47 | }; 48 | 49 | module.exports = BasketProcessor; 50 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/scripts/processors/cookieProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { getCookiesAllowed } = require('*/cartridge/scripts/helpers/sentryHelper'); 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | var SentryOptions = require('*/cartridge/models/SentryOptions'); 7 | // eslint-disable-next-line no-unused-vars 8 | var SentryEvent = require('*/cartridge/models/SentryEvent'); 9 | 10 | /** 11 | * Processor to add cookie information to the request information. 12 | * 13 | * @param {SentryOptions} options - The options 14 | * @constructor 15 | */ 16 | function CookieProcessor(options) { 17 | if (!options) { 18 | throw new Error('The sentryOptions is required.'); 19 | } 20 | 21 | this.options = options; 22 | } 23 | 24 | /** 25 | * Gets the cookies from the HTTP request 26 | * 27 | * @param {dw.system.Request} request - The HTTP request 28 | * @return {string} - The cookies as a string separated by ; 29 | */ 30 | function getCookies(request) { 31 | var cookies = ''; 32 | 33 | if (request.httpCookies) { 34 | // eslint-disable-next-line no-plusplus 35 | for (var i = 0; i < request.httpCookies.cookieCount; i++) { 36 | var currentCookie = request.httpCookies[i]; 37 | 38 | cookies += currentCookie.name + '=' + currentCookie.value + '; '; 39 | } 40 | } 41 | return cookies; 42 | } 43 | 44 | /** 45 | * Processes the event. 46 | * 47 | * @param {SentryEvent} sentryEvent - The event to process 48 | * 49 | * @returns {SentryEvent} - The processed event 50 | */ 51 | CookieProcessor.prototype.process = function (sentryEvent) { 52 | var currentSentryEvent = sentryEvent; 53 | 54 | if (getCookiesAllowed()) { 55 | this.options.logger.debug('Sentry :: Setting cookies on event.'); 56 | 57 | currentSentryEvent.request.cookies = getCookies(request); 58 | } else { 59 | this.options.logger.debug('Sentry :: Not allowed to set cookies on event.'); 60 | } 61 | 62 | return currentSentryEvent; 63 | }; 64 | 65 | module.exports = CookieProcessor; 66 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/scripts/processors/duplicateEventProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | var SentryOptions = require('*/cartridge/models/SentryOptions'); 5 | // eslint-disable-next-line no-unused-vars 6 | var SentryEvent = require('*/cartridge/models/SentryEvent'); 7 | 8 | /** 9 | * Processor to prevent duplicate events being sent to Sentry. 10 | * 11 | * @param {SentryOptions} options - The options 12 | * @constructor 13 | */ 14 | function DuplicateEventProcessor(options) { 15 | if (!options) { 16 | throw new Error('The sentryOptions is required.'); 17 | } 18 | 19 | this.options = options; 20 | } 21 | 22 | /** 23 | * Processes the event. 24 | * 25 | * @param {SentryEvent} sentryEvent - The event to process 26 | * 27 | * @returns {SentryEvent} - The processed event 28 | */ 29 | DuplicateEventProcessor.prototype.process = function (sentryEvent) { 30 | var sentryEventError = sentryEvent.error; 31 | 32 | if (sentryEventError) { 33 | var eventsCache = require('dw/system/CacheMgr').getCache('sentryEvents'); 34 | var Bytes = require('dw/util/Bytes'); 35 | var MessageDigest = require('dw/crypto/MessageDigest'); 36 | var Encoding = require('dw/crypto/Encoding'); 37 | var sessionId = request.session.sessionID; 38 | 39 | this.options.logger.debug('Sentry :: Checking for duplicate events for error: {0}', sentryEventError.message); 40 | 41 | var exceptionHash = Encoding.toHex( 42 | new MessageDigest(MessageDigest.DIGEST_SHA_512).digestBytes( 43 | new Bytes(sessionId + sentryEventError.message + sentryEventError.stack) 44 | ) 45 | ); 46 | 47 | var cacheResult = eventsCache.get(exceptionHash); 48 | 49 | if (cacheResult) { 50 | this.options.logger.debug('Sentry :: Duplicate Exception detected. Event {0} will be discarded.', sentryEvent.event_id); 51 | 52 | return null; 53 | } 54 | 55 | this.options.logger.debug('Sentry :: Exception is not duplicate. Event {0} will pass through.', sentryEvent.event_id); 56 | 57 | eventsCache.put(exceptionHash, true); 58 | } 59 | 60 | return sentryEvent; 61 | }; 62 | 63 | module.exports = DuplicateEventProcessor; 64 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/cartridge/scripts/services/sentryService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Logger = require('dw/system/Logger').getLogger('sentry'); 4 | var DSN_REGEX = 'https://(.+)@.+/(.+)'; 5 | 6 | /** 7 | * Creates the service to add a new Sentry event. 8 | * 9 | * @param {Object} sentryEvent - The exception to post to Sentry 10 | * @param {string} dsn - The DSN to call 11 | * @return {dw.svc.Service} - The service 12 | */ 13 | function sentryEventService(sentryEvent, dsn) { 14 | var { createService } = require('dw/svc/LocalServiceRegistry'); 15 | var sentryConfig = require('*/cartridge/config/sentry'); 16 | 17 | return createService('Sentry', { 18 | createRequest: function (svc) { 19 | var splitDSN = dsn.match(DSN_REGEX); 20 | var key = splitDSN[1]; 21 | var projectID = splitDSN[2]; 22 | var url = dsn.replace(projectID, '') + 'api/' + projectID + '/store/'; 23 | 24 | svc.addHeader('X-Sentry-Auth', 'Sentry sentry_version=7,sentry_key= ' 25 | + key + ',sentry_client=' + sentryConfig['sentry-client'].name + '/' + sentryConfig['sentry-client'].version 26 | + ',sentry_timestamp=' + sentryEvent.timeStamp); 27 | 28 | svc.setRequestMethod('POST'); 29 | svc.setURL(url); 30 | 31 | Logger.debug('Sentry :: Sending event to Sentry: \n {0}.', JSON.stringify(sentryEvent, null, 4)); 32 | 33 | return JSON.stringify(sentryEvent); 34 | }, 35 | parseResponse: function (svc, client) { 36 | try { 37 | if (client.statusCode === 200) { 38 | var SentryId = require('*/cartridge/models/SentryId'); 39 | Logger.debug('Sentry :: Sentry successfully processed our request.'); 40 | 41 | return new SentryId(JSON.parse(client.text).id); 42 | } 43 | } catch (e) { 44 | Logger.error(e); 45 | } 46 | 47 | return null; 48 | }, 49 | mockCall: function () { 50 | if (sentryEvent.exception.values[0].value === 'retry_header') { 51 | return { 52 | statusCode: 429, 53 | statusMessage: 'Maintenance', 54 | responseHeaders: { 55 | 'Retry-After': 1000 56 | }, 57 | text: JSON.stringify({ 58 | id: 'fd6d8c0c43fc4630ad850ee518f1b9d0' 59 | }), 60 | errorText: 'Maintenance' 61 | }; 62 | } 63 | 64 | return { 65 | statusCode: 200, 66 | statusMessage: 'Success', 67 | text: JSON.stringify({ 68 | id: 'fd6d8c0c43fc4630ad850ee518f1b9d0' 69 | }) 70 | }; 71 | } 72 | }); 73 | } 74 | 75 | module.exports = { 76 | sentryEvent: sentryEventService 77 | }; 78 | -------------------------------------------------------------------------------- /cartridges/lib_sentry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "caches": "./caches.json" 3 | } 4 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugin_sentry 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/client/default/js/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jquery": true, 4 | "es6": true 5 | }, 6 | "rules": { 7 | "global-require": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/client/default/js/sentry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(document).ready(function () { 4 | require('./sentry/main').init(); 5 | }); 6 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/client/default/js/sentry/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { init } = require('@sentry/browser'); 4 | 5 | /** 6 | * Determine the instance type of the current Salesforce Commerce Cloud B2C environment. 7 | * 8 | * @return {string|*} The instance type as a string 9 | */ 10 | function determineInstanceType() { 11 | const instanceCode = $('meta[name=sentry-instance]').attr('content'); 12 | const instanceMapping = { 13 | 0: 'development', 14 | 1: 'staging', 15 | 2: 'production' 16 | }; 17 | 18 | if (instanceCode) { 19 | return instanceMapping[instanceCode]; 20 | } 21 | 22 | return 'unknown'; 23 | } 24 | 25 | /** 26 | * Initializes Sentry 27 | */ 28 | function initSentry() { 29 | const DSN = $('meta[name=sentry-dsn]').attr('content'); 30 | const codeVersion = $('meta[name=sentry-version]').attr('content'); 31 | 32 | init({ 33 | dsn: DSN, 34 | release: codeVersion, 35 | environment: determineInstanceType() 36 | }); 37 | } 38 | 39 | module.exports = { 40 | init: initSentry 41 | }; 42 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/controllers/Error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server = require('server'); 4 | 5 | server.extend(module.superModule); 6 | 7 | /** 8 | * Logs the exception to Sentry when the Error page is loaded. 9 | * 10 | * @param {Object} req - Request object 11 | * @param {Object} res - Response object 12 | * @param {Function} next - Next call in the middleware chain 13 | */ 14 | function logError(req, res, next) { 15 | var error = req.error; 16 | 17 | if (error) { 18 | var BasketProcessor = require('*/cartridge/scripts/processors/basketProcessor'); 19 | var Sentry = require('*/cartridge/scripts/Sentry'); 20 | Sentry.getOptions().addEventProcessor(BasketProcessor); 21 | 22 | Sentry.captureException(new Error(error.errorText)); 23 | } 24 | next(); 25 | } 26 | 27 | server.prepend('Start', logError); 28 | server.prepend('ErrorCode', logError); 29 | 30 | /** 31 | * Logs forbidden access requests to Sentry. If a user is logged in, the customer number is logged. 32 | */ 33 | server.prepend('Forbidden', function (req, res, next) { 34 | if (req.currentCustomer.profile) { 35 | var Sentry = require('*/cartridge/scripts/Sentry'); 36 | var message = 'Forbidden access for customer ' + req.currentCustomer.profile.customerNo; 37 | 38 | Sentry.captureException(new Error(message)); 39 | } 40 | 41 | next(); 42 | }); 43 | 44 | module.exports = server.exports(); 45 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/plugin_sentry.properties: -------------------------------------------------------------------------------- 1 | # cartridge.properties for cartridge plugin_sentry 2 | # Tue Feb 23 08:05:34 CET 2021 3 | demandware.cartridges.plugin_sentry.multipleLanguageStorefront=true 4 | demandware.cartridges.plugin_sentry.id=plugin_sentry 5 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/scripts/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": [ 3 | { 4 | "name": "app.template.htmlHead", 5 | "script": "./hooks/sentry/htmlHead.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/scripts/hooks/sentry/htmlHead.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Logger = require('dw/system/Logger').getLogger('sentry'); 4 | 5 | /** 6 | * Adds the Client Side JS for Sentry. 7 | */ 8 | function htmlHead() { 9 | var { getDSN } = require('*/cartridge/scripts/helpers/sentryHelper'); 10 | var sentryConfig = require('*/cartridge/config/sentry'); 11 | var DSN = getDSN(); 12 | 13 | Logger.debug('Sentry :: DSN found with config value: {0}', DSN); 14 | 15 | if (!empty(DSN)) { 16 | var { renderTemplate } = require('dw/template/ISML'); 17 | 18 | renderTemplate('sentry/tag', { 19 | DSN: DSN, 20 | codeVersion: sentryConfig['code-version'] 21 | }); 22 | } 23 | } 24 | 25 | module.exports = { 26 | htmlHead: htmlHead 27 | }; 28 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/static/default/js/sentry.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(r,i,function(e){return t[e]}.bind(null,i));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=14)}([function(t,e,n){"use strict";n.d(e,"a",(function(){return i})),n.d(e,"b",(function(){return c})),n.d(e,"c",(function(){return o}));var r=n(2);const i=n(4).a;function o(t,e){try{let n=t;const r=5,i=80,o=[];let c=0,a=0;const u=" > ",d=u.length;let f;for(;n&&c++1&&a+o.length*d+f.length>=i));)o.push(f),a+=f.length,n=n.parentNode;return o.reverse().join(u)}catch(t){return""}}function s(t,e){const n=t,i=[];let o,s,c,a,u;if(!n||!n.tagName)return"";i.push(n.tagName.toLowerCase());const d=e&&e.length?e.filter(t=>n.getAttribute(t)).map(t=>[t,n.getAttribute(t)]):null;if(d&&d.length)d.forEach(t=>{i.push(`[${t[0]}="${t[1]}"]`)});else if(n.id&&i.push("#"+n.id),o=n.className,o&&Object(r.l)(o))for(s=o.split(/\s+/),u=0;u`${encodeURIComponent(e)}=${encodeURIComponent(t[e])}`).join("&")}function f(t){if(Object(i.d)(t))return{message:t.message,name:t.name,stack:t.stack,...l(t)};if(Object(i.f)(t)){const e={type:t.type,target:_(t.target),currentTarget:_(t.currentTarget),...l(t)};return"undefined"!=typeof CustomEvent&&Object(i.g)(t,CustomEvent)&&(e.detail=t.detail),e}return t}function _(t){try{return Object(i.c)(t)?Object(r.c)(t):Object.prototype.toString.call(t)}catch(t){return""}}function l(t){if("object"==typeof t&&null!==t){const e={};for(const n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e}return{}}function p(t,e=40){const n=Object.keys(f(t));if(n.sort(),!n.length)return"[object has no keys]";if(n[0].length>=e)return Object(o.d)(n[0],e);for(let t=n.length;t>0;t--){const r=n.slice(0,t).join(", ");if(!(r.length>e))return t===n.length?r:Object(o.d)(r,e)}return""}function h(t){return function t(e,n){if(Object(i.i)(e)){const r=n.get(e);if(void 0!==r)return r;const i={};n.set(e,i);for(const r of Object.keys(e))void 0!==e[r]&&(i[r]=t(e[r],n));return i}if(Array.isArray(e)){const r=n.get(e);if(void 0!==r)return r;const i=[];return n.set(e,i),e.forEach(e=>{i.push(t(e,n))}),i}return e}(t,new Map)}},function(t,e,n){"use strict";(function(t){function r(t){return t&&t.Math==Math?t:void 0}n.d(e,"a",(function(){return i})),n.d(e,"b",(function(){return o}));const i="object"==typeof globalThis&&r(globalThis)||"object"==typeof window&&r(window)||"object"==typeof self&&r(self)||"object"==typeof t&&r(t)||function(){return this}()||{};function o(t,e,n){const r=n||i,o=r.__SENTRY__=r.__SENTRY__||{};return o[t]||(o[t]=e())}}).call(this,n(8))},function(t,e,n){"use strict";n.d(e,"a",(function(){return c})),n.d(e,"b",(function(){return s})),n.d(e,"c",(function(){return o})),n.d(e,"d",(function(){return i}));var r=n(2);function i(t,e=0){return"string"!=typeof t||0===e||t.length<=e?t:t.substr(0,e)+"..."}function o(t,e){let n=t;const r=n.length;if(r<=150)return n;e>r&&(e=r);let i=Math.max(e-60,0);i<5&&(i=0);let o=Math.min(i+140,r);return o>r-5&&(o=r),o===r&&(i=Math.max(o-140,0)),n=n.slice(i,o),i>0&&(n="'{snip} "+n),ot[0]-e[0]).map(t=>t[1]);return(t,n=0)=>{const r=[];for(const i of t.split("\n").slice(n)){const t=i.replace(/\(error: (.*)\)/,"$1");for(const n of e){const e=n(t);if(e){r.push(e);break}}}return function(t){if(!t.length)return[];let e=t;const n=e[0].function||"",r=e[e.length-1].function||"";-1===n.indexOf("captureMessage")&&-1===n.indexOf("captureException")||(e=e.slice(1));-1!==r.indexOf("sentryWrapped")&&(e=e.slice(0,-1));return e.slice(0,50).map(t=>({...t,filename:t.filename||e[0].filename,function:t.function||"?"})).reverse()}(r)}}function i(t){return Array.isArray(t)?r(...t):t}function o(t){try{return t&&"function"==typeof t&&t.name||""}catch(t){return""}}},function(t,e,n){"use strict";(function(t,r){n.d(e,"a",(function(){return s})),n.d(e,"b",(function(){return o}));var i=n(11);function o(){return!Object(i.a)()&&"[object process]"===Object.prototype.toString.call(void 0!==t?t:0)}function s(t,e){return t.require(e)}}).call(this,n(17),n(9)(t))},function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(t){"object"==typeof window&&(n=window)}t.exports=n},function(t,e){t.exports=function(t){if(!t.webpackPolyfill){var e=Object.create(t);e.children||(e.children=[]),Object.defineProperty(e,"loaded",{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,"id",{enumerable:!0,get:function(){return e.i}}),Object.defineProperty(e,"exports",{enumerable:!0}),e.webpackPolyfill=1}return e}},function(t,e,n){"use strict";(function(t){n.d(e,"a",(function(){return a})),n.d(e,"b",(function(){return u}));var r=n(0),i=n(7);const o={nowSeconds:()=>Date.now()/1e3};const s=Object(i.b)()?function(){try{return Object(i.a)(t,"perf_hooks").performance}catch(t){return}}():function(){const{performance:t}=r.a;if(!t||!t.now)return;return{now:()=>t.now(),timeOrigin:Date.now()-t.now()}}(),c=void 0===s?o:{nowSeconds:()=>(s.timeOrigin+s.now())/1e3},a=o.nowSeconds.bind(o),u=c.nowSeconds.bind(c);let d;(()=>{const{performance:t}=r.a;if(!t||!t.now)return void(d="none");const e=t.now(),n=Date.now(),i=t.timeOrigin?Math.abs(t.timeOrigin+e-n):36e5,o=i<36e5,s=t.timing&&t.timing.navigationStart,c="number"==typeof s?Math.abs(s+e-n):36e5;o||c<36e5?i<=c?(d="timeOrigin",t.timeOrigin):d="navigationStart":d="dateNow"})()}).call(this,n(9)(t))},function(t,e,n){"use strict";function r(){return"undefined"!=typeof __SENTRY_BROWSER_BUNDLE__&&!!__SENTRY_BROWSER_BUNDLE__}n.d(e,"a",(function(){return r}))},function(t,e,n){"use strict";(function(t){n.d(e,"a",(function(){return c})),n.d(e,"b",(function(){return a}));var r=n(2),i=n(13),o=n(3),s=n(6);function c(e,n=1/0,c=1/0){try{return function e(n,c,a=1/0,u=1/0,d=Object(i.a)()){const[f,_]=d;if(null===c||["number","boolean","string"].includes(typeof c)&&!Object(r.h)(c))return c;const l=function(e,n){try{return"domain"===e&&n&&"object"==typeof n&&n._events?"[Domain]":"domainEmitter"===e?"[DomainEmitter]":void 0!==t&&n===t?"[Global]":"undefined"!=typeof window&&n===window?"[Window]":"undefined"!=typeof document&&n===document?"[Document]":Object(r.m)(n)?"[SyntheticEvent]":"number"==typeof n&&n!=n?"[NaN]":void 0===n?"[undefined]":"function"==typeof n?`[Function: ${Object(s.b)(n)}]`:"symbol"==typeof n?`[${String(n)}]`:"bigint"==typeof n?`[BigInt: ${String(n)}]`:`[object ${Object.getPrototypeOf(n).constructor.name}]`}catch(t){return`**non-serializable** (${t})`}}(n,c);if(!l.startsWith("[object "))return l;if(c.__sentry_skip_normalization__)return c;if(0===a)return l.replace("object ","");if(f(c))return"[Circular ~]";const p=c;if(p&&"function"==typeof p.toJSON)try{const t=p.toJSON();return e("",t,a-1,u,d)}catch(t){}const h=Array.isArray(c)?[]:{};let g=0;const m=Object(o.b)(c);for(const t in m){if(!Object.prototype.hasOwnProperty.call(m,t))continue;if(g>=u){h[t]="[MaxProperties ~]";break}const n=m[t];h[t]=e(t,n,a-1,u,d),g+=1}return _(c),h}("",e,n,c)}catch(t){return{ERROR:`**non-serializable** (${t})`}}}function a(t,e=3,n=102400){const r=c(t,e);return i=r,function(t){return~-encodeURI(t).split(/%..|./).length}(JSON.stringify(i))>n?a(t,e-1,n):r;var i}}).call(this,n(8))},function(t,e,n){"use strict";function r(){const t="function"==typeof WeakSet,e=t?new WeakSet:[];return[function(n){if(t)return!!e.has(n)||(e.add(n),!1);for(let t=0;t1)for(var n=1;n{const r=e[t]&&e[t].__sentry_original__;t in e&&r&&(n[t]=e[t],e[t]=r)});try{return t()}finally{Object.keys(n).forEach(t=>{e[t]=n[t]})}}function f(){let t=!1;const e={enable:()=>{t=!0},disable:()=>{t=!1}};return"undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__?u.forEach(n=>{e[n]=(...e)=>{t&&d(()=>{a.a.console[n](`Sentry Logger [${n}]:`,...e)})}}):u.forEach(t=>{e[t]=()=>{}}),e}let _;_="undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__?Object(a.b)("logger",f):f();var l=n(5);function p(){const t=a.a,e=t.crypto||t.msCrypto;if(e&&e.randomUUID)return e.randomUUID().replace(/-/g,"");const n=e&&e.getRandomValues?()=>e.getRandomValues(new Uint8Array(1))[0]:()=>16*Math.random();return([1e7]+1e3+4e3+8e3+1e11).replace(/[018]/g,t=>(t^(15&n())>>t/4).toString(16))}function h(t){return t.exception&&t.exception.values?t.exception.values[0]:void 0}function g(t){const{message:e,event_id:n}=t;if(e)return e;const r=h(t);return r?r.type&&r.value?`${r.type}: ${r.value}`:r.type||r.value||n||"":n||""}function m(t,e,n){const r=t.exception=t.exception||{},i=r.values=r.values||[],o=i[0]=i[0]||{};o.value||(o.value=e||""),o.type||(o.type=n||"Error")}function y(t,e){const n=h(t);if(!n)return;const r=n.mechanism;if(n.mechanism={type:"generic",handled:!0,...r,...e},e&&"data"in e){const t={...r&&r.data,...e.data};n.mechanism.data=t}}function v(t){if(t&&t.__sentry_captured__)return!0;try{Object(o.a)(t,"__sentry_captured__",!0)}catch(t){}return!1}function E(t){return Array.isArray(t)?t:[t]}const b=[/^Script error\.?$/,/^Javascript error: Script error\.? on line 0$/];class S{static __initStatic(){this.id="InboundFilters"}__init(){this.name=S.id}constructor(t={}){this._options=t,S.prototype.__init.call(this)}setupOnce(t,e){const n=t=>{const n=e();if(n){const e=n.getIntegration(S);if(e){const r=n.getClient(),i=r?r.getOptions():{};return function(t,e){if(e.ignoreInternal&&function(t){try{return"SentryError"===t.exception.values[0].type}catch(t){}return!1}(t))return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Event dropped due to being internal Sentry Error.\nEvent: "+g(t)),!0;if(function(t,e){if(!e||!e.length)return!1;return function(t){if(t.message)return[t.message];if(t.exception)try{const{type:e="",value:n=""}=t.exception.values&&t.exception.values[0]||{};return[""+n,`${e}: ${n}`]}catch(e){return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Cannot extract message for event "+g(t)),[]}return[]}(t).some(t=>e.some(e=>Object(l.a)(t,e)))}(t,e.ignoreErrors))return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Event dropped due to being matched by `ignoreErrors` option.\nEvent: "+g(t)),!0;if(function(t,e){if(!e||!e.length)return!1;const n=O(t);return!!n&&e.some(t=>Object(l.a)(n,t))}(t,e.denyUrls))return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn(`Event dropped due to being matched by \`denyUrls\` option.\nEvent: ${g(t)}.\nUrl: ${O(t)}`),!0;if(!function(t,e){if(!e||!e.length)return!0;const n=O(t);return!n||e.some(t=>Object(l.a)(n,t))}(t,e.allowUrls))return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn(`Event dropped due to not being matched by \`allowUrls\` option.\nEvent: ${g(t)}.\nUrl: ${O(t)}`),!0;return!1}(t,function(t={},e={}){return{allowUrls:[...t.allowUrls||[],...e.allowUrls||[]],denyUrls:[...t.denyUrls||[],...e.denyUrls||[]],ignoreErrors:[...t.ignoreErrors||[],...e.ignoreErrors||[],...b],ignoreInternal:void 0===t.ignoreInternal||t.ignoreInternal}}(e._options,i))?null:t}}return t};n.id=this.name,t(n)}}function O(t){try{let e;try{e=t.exception.values[0].stacktrace.frames}catch(t){}return e?function(t=[]){for(let e=t.length-1;e>=0;e--){const n=t[e];if(n&&""!==n.filename&&"[native code]"!==n.filename)return n.filename||null}return null}(e):null}catch(e){return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Cannot extract url for event "+g(t)),null}}S.__initStatic();var w,x=n(10),T=n(7),j=n(2);function D(t){return new k(e=>{e(t)})}function R(t){return new k((e,n)=>{n(t)})}!function(t){t[t.PENDING=0]="PENDING";t[t.RESOLVED=1]="RESOLVED";t[t.REJECTED=2]="REJECTED"}(w||(w={}));class k{__init(){this._state=w.PENDING}__init2(){this._handlers=[]}constructor(t){k.prototype.__init.call(this),k.prototype.__init2.call(this),k.prototype.__init3.call(this),k.prototype.__init4.call(this),k.prototype.__init5.call(this),k.prototype.__init6.call(this);try{t(this._resolve,this._reject)}catch(t){this._reject(t)}}then(t,e){return new k((n,r)=>{this._handlers.push([!1,e=>{if(t)try{n(t(e))}catch(t){r(t)}else n(e)},t=>{if(e)try{n(e(t))}catch(t){r(t)}else r(t)}]),this._executeHandlers()})}catch(t){return this.then(t=>t,t)}finally(t){return new k((e,n)=>{let r,i;return this.then(e=>{i=!1,r=e,t&&t()},e=>{i=!0,r=e,t&&t()}).then(()=>{i?n(r):e(r)})})}__init3(){this._resolve=t=>{this._setResult(w.RESOLVED,t)}}__init4(){this._reject=t=>{this._setResult(w.REJECTED,t)}}__init5(){this._setResult=(t,e)=>{this._state===w.PENDING&&(Object(j.n)(e)?e.then(this._resolve,this._reject):(this._state=t,this._value=e,this._executeHandlers()))}}__init6(){this._executeHandlers=()=>{if(this._state===w.PENDING)return;const t=this._handlers.slice();this._handlers=[],t.forEach(t=>{t[0]||(this._state===w.RESOLVED&&t[1](this._value),this._state===w.REJECTED&&t[2](this._value),t[0]=!0)})}}}function N(t){const e=Object(x.b)(),n={sid:p(),init:!0,timestamp:e,started:e,duration:0,status:"ok",errors:0,ignoreDuration:!1,toJSON:()=>function(t){return Object(o.c)({sid:""+t.sid,init:t.init,started:new Date(1e3*t.started).toISOString(),timestamp:new Date(1e3*t.timestamp).toISOString(),status:t.status,errors:t.errors,did:"number"==typeof t.did||"string"==typeof t.did?""+t.did:void 0,duration:t.duration,attrs:{release:t.release,environment:t.environment,ip_address:t.ipAddress,user_agent:t.userAgent}})}(n)};return t&&U(n,t),n}function U(t,e={}){if(e.user&&(!t.ipAddress&&e.user.ip_address&&(t.ipAddress=e.user.ip_address),t.did||e.did||(t.did=e.user.id||e.user.email||e.user.username)),t.timestamp=e.timestamp||Object(x.b)(),e.ignoreDuration&&(t.ignoreDuration=e.ignoreDuration),e.sid&&(t.sid=32===e.sid.length?e.sid:p()),void 0!==e.init&&(t.init=e.init),!t.did&&e.did&&(t.did=""+e.did),"number"==typeof e.started&&(t.started=e.started),t.ignoreDuration)t.duration=void 0;else if("number"==typeof e.duration)t.duration=e.duration;else{const e=t.timestamp-t.started;t.duration=e>=0?e:0}e.release&&(t.release=e.release),e.environment&&(t.environment=e.environment),!t.ipAddress&&e.ipAddress&&(t.ipAddress=e.ipAddress),!t.userAgent&&e.userAgent&&(t.userAgent=e.userAgent),"number"==typeof e.errors&&(t.errors=e.errors),e.status&&(t.status=e.status)}class B{constructor(){this._notifyingListeners=!1,this._scopeListeners=[],this._eventProcessors=[],this._breadcrumbs=[],this._attachments=[],this._user={},this._tags={},this._extra={},this._contexts={},this._sdkProcessingMetadata={}}static clone(t){const e=new B;return t&&(e._breadcrumbs=[...t._breadcrumbs],e._tags={...t._tags},e._extra={...t._extra},e._contexts={...t._contexts},e._user=t._user,e._level=t._level,e._span=t._span,e._session=t._session,e._transactionName=t._transactionName,e._fingerprint=t._fingerprint,e._eventProcessors=[...t._eventProcessors],e._requestSession=t._requestSession,e._attachments=[...t._attachments]),e}addScopeListener(t){this._scopeListeners.push(t)}addEventProcessor(t){return this._eventProcessors.push(t),this}setUser(t){return this._user=t||{},this._session&&U(this._session,{user:t}),this._notifyScopeListeners(),this}getUser(){return this._user}getRequestSession(){return this._requestSession}setRequestSession(t){return this._requestSession=t,this}setTags(t){return this._tags={...this._tags,...t},this._notifyScopeListeners(),this}setTag(t,e){return this._tags={...this._tags,[t]:e},this._notifyScopeListeners(),this}setExtras(t){return this._extra={...this._extra,...t},this._notifyScopeListeners(),this}setExtra(t,e){return this._extra={...this._extra,[t]:e},this._notifyScopeListeners(),this}setFingerprint(t){return this._fingerprint=t,this._notifyScopeListeners(),this}setLevel(t){return this._level=t,this._notifyScopeListeners(),this}setTransactionName(t){return this._transactionName=t,this._notifyScopeListeners(),this}setContext(t,e){return null===e?delete this._contexts[t]:this._contexts={...this._contexts,[t]:e},this._notifyScopeListeners(),this}setSpan(t){return this._span=t,this._notifyScopeListeners(),this}getSpan(){return this._span}getTransaction(){const t=this.getSpan();return t&&t.transaction}setSession(t){return t?this._session=t:delete this._session,this._notifyScopeListeners(),this}getSession(){return this._session}update(t){if(!t)return this;if("function"==typeof t){const e=t(this);return e instanceof B?e:this}return t instanceof B?(this._tags={...this._tags,...t._tags},this._extra={...this._extra,...t._extra},this._contexts={...this._contexts,...t._contexts},t._user&&Object.keys(t._user).length&&(this._user=t._user),t._level&&(this._level=t._level),t._fingerprint&&(this._fingerprint=t._fingerprint),t._requestSession&&(this._requestSession=t._requestSession)):Object(j.i)(t)&&(t=t,this._tags={...this._tags,...t.tags},this._extra={...this._extra,...t.extra},this._contexts={...this._contexts,...t.contexts},t.user&&(this._user=t.user),t.level&&(this._level=t.level),t.fingerprint&&(this._fingerprint=t.fingerprint),t.requestSession&&(this._requestSession=t.requestSession)),this}clear(){return this._breadcrumbs=[],this._tags={},this._extra={},this._user={},this._contexts={},this._level=void 0,this._transactionName=void 0,this._fingerprint=void 0,this._requestSession=void 0,this._span=void 0,this._session=void 0,this._notifyScopeListeners(),this._attachments=[],this}addBreadcrumb(t,e){const n="number"==typeof e?e:100;if(n<=0)return this;const r={timestamp:Object(x.a)(),...t};return this._breadcrumbs=[...this._breadcrumbs,r].slice(-n),this._notifyScopeListeners(),this}clearBreadcrumbs(){return this._breadcrumbs=[],this._notifyScopeListeners(),this}addAttachment(t){return this._attachments.push(t),this}getAttachments(){return this._attachments}clearAttachments(){return this._attachments=[],this}applyToEvent(t,e={}){if(this._extra&&Object.keys(this._extra).length&&(t.extra={...this._extra,...t.extra}),this._tags&&Object.keys(this._tags).length&&(t.tags={...this._tags,...t.tags}),this._user&&Object.keys(this._user).length&&(t.user={...this._user,...t.user}),this._contexts&&Object.keys(this._contexts).length&&(t.contexts={...this._contexts,...t.contexts}),this._level&&(t.level=this._level),this._transactionName&&(t.transaction=this._transactionName),this._span){t.contexts={trace:this._span.getTraceContext(),...t.contexts};const e=this._span.transaction&&this._span.transaction.name;e&&(t.tags={transaction:e,...t.tags})}return this._applyFingerprint(t),t.breadcrumbs=[...t.breadcrumbs||[],...this._breadcrumbs],t.breadcrumbs=t.breadcrumbs.length>0?t.breadcrumbs:void 0,t.sdkProcessingMetadata={...t.sdkProcessingMetadata,...this._sdkProcessingMetadata},this._notifyEventProcessors([...I(),...this._eventProcessors],t,e)}setSDKProcessingMetadata(t){return this._sdkProcessingMetadata={...this._sdkProcessingMetadata,...t},this}_notifyEventProcessors(t,e,n,r=0){return new k((i,o)=>{const s=t[r];if(null===e||"function"!=typeof s)i(e);else{const c=s({...e},n);("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&s.id&&null===c&&_.log(`Event processor "${s.id}" dropped event`),Object(j.n)(c)?c.then(e=>this._notifyEventProcessors(t,e,n,r+1).then(i)).then(null,o):this._notifyEventProcessors(t,c,n,r+1).then(i).then(null,o)}})}_notifyScopeListeners(){this._notifyingListeners||(this._notifyingListeners=!0,this._scopeListeners.forEach(t=>{t(this)}),this._notifyingListeners=!1)}_applyFingerprint(t){t.fingerprint=t.fingerprint?E(t.fingerprint):[],this._fingerprint&&(t.fingerprint=t.fingerprint.concat(this._fingerprint)),t.fingerprint&&!t.fingerprint.length&&delete t.fingerprint}}function I(){return Object(a.b)("globalEventProcessors",()=>[])}function Y(t){I().push(t)}const G=100;class C{__init(){this._stack=[{}]}constructor(t,e=new B,n=4){this._version=n,C.prototype.__init.call(this),this.getStackTop().scope=e,t&&this.bindClient(t)}isOlderThan(t){return this._version{i.captureException(t,{originalException:t,syntheticException:r,...e,event_id:n},o)}),n}captureMessage(t,e,n){const r=this._lastEventId=n&&n.event_id?n.event_id:p(),i=new Error(t);return this._withClient((o,s)=>{o.captureMessage(t,e,{originalException:t,syntheticException:i,...n,event_id:r},s)}),r}captureEvent(t,e){const n=e&&e.event_id?e.event_id:p();return"transaction"!==t.type&&(this._lastEventId=n),this._withClient((r,i)=>{r.captureEvent(t,{...e,event_id:n},i)}),n}lastEventId(){return this._lastEventId}addBreadcrumb(t,e){const{scope:n,client:r}=this.getStackTop();if(!n||!r)return;const{beforeBreadcrumb:i=null,maxBreadcrumbs:o=G}=r.getOptions&&r.getOptions()||{};if(o<=0)return;const s={timestamp:Object(x.a)(),...t},c=i?d(()=>i(s,e)):s;null!==c&&n.addBreadcrumb(c,o)}setUser(t){const e=this.getScope();e&&e.setUser(t)}setTags(t){const e=this.getScope();e&&e.setTags(t)}setExtras(t){const e=this.getScope();e&&e.setExtras(t)}setTag(t,e){const n=this.getScope();n&&n.setTag(t,e)}setExtra(t,e){const n=this.getScope();n&&n.setExtra(t,e)}setContext(t,e){const n=this.getScope();n&&n.setContext(t,e)}configureScope(t){const{scope:e,client:n}=this.getStackTop();e&&n&&t(e)}run(t){const e=L(this);try{t(this)}finally{L(e)}}getIntegration(t){const e=this.getClient();if(!e)return null;try{return e.getIntegration(t)}catch(e){return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn(`Cannot retrieve integration ${t.id} from the current Hub`),null}}startTransaction(t,e){return this._callExtensionMethod("startTransaction",t,e)}traceHeaders(){return this._callExtensionMethod("traceHeaders")}captureSession(t=!1){if(t)return this.endSession();this._sendSessionUpdate()}endSession(){const t=this.getStackTop(),e=t&&t.scope,n=e&&e.getSession();n&&function(t,e){let n={};e?n={status:e}:"ok"===t.status&&(n={status:"exited"}),U(t,n)}(n),this._sendSessionUpdate(),e&&e.setSession()}startSession(t){const{scope:e,client:n}=this.getStackTop(),{release:r,environment:i}=n&&n.getOptions()||{},{userAgent:o}=a.a.navigator||{},s=N({release:r,environment:i,...e&&{user:e.getUser()},...o&&{userAgent:o},...t});if(e){const t=e.getSession&&e.getSession();t&&"ok"===t.status&&U(t,{status:"exited"}),this.endSession(),e.setSession(s)}return s}shouldSendDefaultPii(){const t=this.getClient(),e=t&&t.getOptions();return Boolean(e&&e.sendDefaultPii)}_sendSessionUpdate(){const{scope:t,client:e}=this.getStackTop();if(!t)return;const n=t.getSession();n&&e&&e.captureSession&&e.captureSession(n)}_withClient(t){const{scope:e,client:n}=this.getStackTop();n&&t(n,e)}_callExtensionMethod(t,...e){const n=P().__SENTRY__;if(n&&n.extensions&&"function"==typeof n.extensions[t])return n.extensions[t].apply(this,e);("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn(`Extension method ${t} couldn't be found, doing nothing.`)}}function P(){return a.a.__SENTRY__=a.a.__SENTRY__||{extensions:{},hub:void 0},a.a}function L(t){const e=P(),n=A(e);return q(e,t),n}function $(){const t=P();return M(t)&&!A(t).isOlderThan(4)||q(t,new C),Object(T.b)()?function(t){try{const e=P().__SENTRY__,n=e&&e.extensions&&e.extensions.domain&&e.extensions.domain.active;if(!n)return A(t);if(!M(n)||A(n).isOlderThan(4)){const e=A(t).getStackTop();q(n,new C(e.client,B.clone(e.scope)))}return A(n)}catch(e){return A(t)}}(t):A(t)}function M(t){return!!(t&&t.__SENTRY__&&t.__SENTRY__.hub)}function A(t){return Object(a.b)("hub",()=>new C,t)}function q(t,e){if(!t)return!1;return(t.__SENTRY__=t.__SENTRY__||{}).hub=e,!0}const F="7.16.0";function H(t,e){return $().captureException(t,{captureContext:e})}function z(t,e){const n="string"==typeof e?e:void 0,r="string"!=typeof e?{captureContext:e}:void 0;return $().captureMessage(t,n,r)}function W(t,e){return $().captureEvent(t,e)}function X(t){$().configureScope(t)}function J(t){$().addBreadcrumb(t)}function K(t,e){$().setContext(t,e)}function V(t){$().setExtras(t)}function Q(t,e){$().setExtra(t,e)}function Z(t){$().setTags(t)}function tt(t,e){$().setTag(t,e)}function et(t){$().setUser(t)}function nt(t){$().withScope(t)}function rt(t,e){return $().startTransaction({...t},e)}class it extends Error{constructor(t,e="warn"){super(t),this.message=t,this.name=new.target.prototype.constructor.name,Object.setPrototypeOf(this,new.target.prototype),this.logLevel=e}}function ot(t){const e=[];function n(t){return e.splice(e.indexOf(t),1)[0]}return{$:e,add:function(r){if(!(void 0===t||e.lengthn(i)).then(null,()=>n(i).then(null,()=>{})),i},drain:function(t){return new k((n,r)=>{let i=e.length;if(!i)return n(!0);const o=setTimeout(()=>{t&&t>0&&n(!1)},t);e.forEach(t=>{D(t).then(()=>{--i||(clearTimeout(o),n(!0))},r)})})}}}var st=n(12);function ct(t,e=[]){return[t,e]}function at(t,e){const[n,r]=t;return[n,[...r,e]]}function ut(t,e){t[1].forEach(t=>{const n=t[0].type;e(t,n)})}function dt(t,e){return(e||new TextEncoder).encode(t)}function ft(t,e){const[n,r]=t;let i=JSON.stringify(n);function o(t){"string"==typeof i?i="string"==typeof t?i+t:[dt(i,e),t]:i.push("string"==typeof t?dt(t,e):t)}for(const t of r){const[e,n]=t;if(o(`\n${JSON.stringify(e)}\n`),"string"==typeof n||n instanceof Uint8Array)o(n);else{let t;try{t=JSON.stringify(n)}catch(e){t=JSON.stringify(Object(st.a)(n))}o(t)}}return"string"==typeof i?i:function(t){const e=t.reduce((t,e)=>t+e.length,0),n=new Uint8Array(e);let r=0;for(const e of t)n.set(e,r),r+=e.length;return n}(i)}function _t(t,e){const n="string"==typeof t.data?dt(t.data,e):t.data;return[Object(o.c)({type:"attachment",length:n.length,filename:t.filename,content_type:t.contentType,attachment_type:t.attachmentType}),n]}const lt={session:"session",sessions:"session",attachment:"attachment",transaction:"transaction",event:"error",client_report:"internal",user_report:"default"};function pt(t){return lt[t]}function ht(t,{statusCode:e,headers:n},r=Date.now()){const i={...t},o=n&&n["x-sentry-rate-limits"],s=n&&n["retry-after"];if(o)for(const t of o.trim().split(",")){const[e,n]=t.split(":",2),o=parseInt(e,10),s=1e3*(isNaN(o)?60:o);if(n)for(const t of n.split(";"))i[t]=r+s;else i.all=r+s}else s?i.all=r+function(t,e=Date.now()){const n=parseInt(""+t,10);if(!isNaN(n))return 1e3*n;const r=Date.parse(""+t);return isNaN(r)?6e4:r-e}(s,r):429===e&&(i.all=r+6e4);return i}function gt(t,e,n=ot(t.bufferSize||30)){let r={};return{send:function(i){const o=[];if(ut(i,(e,n)=>{const i=pt(n);!function(t,e,n=Date.now()){return function(t,e){return t[e]||t.all||0}(t,e)>n}(r,i)?o.push(e):t.recordDroppedEvent("ratelimit_backoff",i)}),0===o.length)return D();const s=ct(i[0],o),c=e=>{ut(s,(n,r)=>{t.recordDroppedEvent(e,pt(r))})};return n.add(()=>e({body:ft(s,t.textEncoder)}).then(t=>{void 0!==t.statusCode&&(t.statusCode<200||t.statusCode>=300)&&("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn(`Sentry responded with status code ${t.statusCode} to sent event.`),r=ht(r,t)},t=>{("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Failed while sending event:",t),c("network_error")})).then(t=>t,t=>{if(t instanceof it)return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Skipped sending event because buffer is full."),c("queue_overflow"),D();throw t})},flush:t=>n.drain(t)}}var mt=n(0),yt=n(6);function vt(){if(!("fetch"in mt.a))return!1;try{return new Headers,new Request("http://www.example.com"),new Response,!0}catch(t){return!1}}function Et(t){return t&&/^function fetch\(\)\s+\{\s+\[native code\]\s+\}$/.test(t.toString())}const bt={},St={};function Ot(t){if(!St[t])switch(St[t]=!0,t){case"console":!function(){if(!("console"in mt.a))return;u.forEach((function(t){t in mt.a.console&&Object(o.e)(mt.a.console,t,(function(e){return function(...n){xt("console",{args:n,level:t}),e&&e.apply(mt.a.console,n)}}))}))}();break;case"dom":!function(){if(!("document"in mt.a))return;const t=xt.bind(null,"dom"),e=Nt(t,!0);mt.a.document.addEventListener("click",e,!1),mt.a.document.addEventListener("keypress",e,!1),["EventTarget","Node"].forEach(e=>{const n=mt.a[e]&&mt.a[e].prototype;n&&n.hasOwnProperty&&n.hasOwnProperty("addEventListener")&&(Object(o.e)(n,"addEventListener",(function(e){return function(n,r,i){if("click"===n||"keypress"==n)try{const r=this,o=r.__sentry_instrumentation_handlers__=r.__sentry_instrumentation_handlers__||{},s=o[n]=o[n]||{refCount:0};if(!s.handler){const r=Nt(t);s.handler=r,e.call(this,n,r,i)}s.refCount+=1}catch(t){}return e.call(this,n,r,i)}})),Object(o.e)(n,"removeEventListener",(function(t){return function(e,n,r){if("click"===e||"keypress"==e)try{const n=this,i=n.__sentry_instrumentation_handlers__||{},o=i[e];o&&(o.refCount-=1,o.refCount<=0&&(t.call(this,e,o.handler,r),o.handler=void 0,delete i[e]),0===Object.keys(i).length&&delete n.__sentry_instrumentation_handlers__)}catch(t){}return t.call(this,e,n,r)}})))})}();break;case"xhr":!function(){if(!("XMLHttpRequest"in mt.a))return;const t=XMLHttpRequest.prototype;Object(o.e)(t,"open",(function(t){return function(...e){const n=this,r=e[1],i=n.__sentry_xhr__={method:Object(j.l)(e[0])?e[0].toUpperCase():e[0],url:e[1]};Object(j.l)(r)&&"POST"===i.method&&r.match(/sentry_key/)&&(n.__sentry_own_request__=!0);const s=function(){if(4===n.readyState){try{i.status_code=n.status}catch(t){}xt("xhr",{args:e,endTimestamp:Date.now(),startTimestamp:Date.now(),xhr:n})}};return"onreadystatechange"in n&&"function"==typeof n.onreadystatechange?Object(o.e)(n,"onreadystatechange",(function(t){return function(...e){return s(),t.apply(n,e)}})):n.addEventListener("readystatechange",s),t.apply(n,e)}})),Object(o.e)(t,"send",(function(t){return function(...e){return this.__sentry_xhr__&&void 0!==e[0]&&(this.__sentry_xhr__.body=e[0]),xt("xhr",{args:e,startTimestamp:Date.now(),xhr:this}),t.apply(this,e)}}))}();break;case"fetch":!function(){if(!function(){if(!vt())return!1;if(Et(mt.a.fetch))return!0;let t=!1;const e=mt.a.document;if(e&&"function"==typeof e.createElement)try{const n=e.createElement("iframe");n.hidden=!0,e.head.appendChild(n),n.contentWindow&&n.contentWindow.fetch&&(t=Et(n.contentWindow.fetch)),e.head.removeChild(n)}catch(t){("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ",t)}return t}())return;Object(o.e)(mt.a,"fetch",(function(t){return function(...e){const n={args:e,fetchData:{method:Tt(e),url:jt(e)},startTimestamp:Date.now()};return xt("fetch",{...n}),t.apply(mt.a,e).then(t=>(xt("fetch",{...n,endTimestamp:Date.now(),response:t}),t),t=>{throw xt("fetch",{...n,endTimestamp:Date.now(),error:t}),t})}}))}();break;case"history":!function(){if(!function(){const t=mt.a.chrome,e=t&&t.app&&t.app.runtime,n="history"in mt.a&&!!mt.a.history.pushState&&!!mt.a.history.replaceState;return!e&&n}())return;const t=mt.a.onpopstate;function e(t){return function(...e){const n=e.length>2?e[2]:void 0;if(n){const t=Dt,e=String(n);Dt=e,xt("history",{from:t,to:e})}return t.apply(this,e)}}mt.a.onpopstate=function(...e){const n=mt.a.location.href,r=Dt;if(Dt=n,xt("history",{from:r,to:n}),t)try{return t.apply(this,e)}catch(t){}},Object(o.e)(mt.a.history,"pushState",e),Object(o.e)(mt.a.history,"replaceState",e)}();break;case"error":Ut=mt.a.onerror,mt.a.onerror=function(t,e,n,r,i){return xt("error",{column:r,error:i,line:n,msg:t,url:e}),!!Ut&&Ut.apply(this,arguments)};break;case"unhandledrejection":Bt=mt.a.onunhandledrejection,mt.a.onunhandledrejection=function(t){return xt("unhandledrejection",t),!Bt||Bt.apply(this,arguments)};break;default:return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("unknown instrumentation type:",t))}}function wt(t,e){bt[t]=bt[t]||[],bt[t].push(e),Ot(t)}function xt(t,e){if(t&&bt[t])for(const n of bt[t]||[])try{n(e)}catch(e){("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error(`Error while triggering instrumentation handler.\nType: ${t}\nName: ${Object(yt.b)(n)}\nError:`,e)}}function Tt(t=[]){return"Request"in mt.a&&Object(j.g)(t[0],Request)&&t[0].method?String(t[0].method).toUpperCase():t[1]&&t[1].method?String(t[1].method).toUpperCase():"GET"}function jt(t=[]){return"string"==typeof t[0]?t[0]:"Request"in mt.a&&Object(j.g)(t[0],Request)?t[0].url:String(t[0])}let Dt;let Rt,kt;function Nt(t,e=!1){return n=>{if(!n||kt===n)return;if(function(t){if("keypress"!==t.type)return!1;try{const e=t.target;if(!e||!e.tagName)return!0;if("INPUT"===e.tagName||"TEXTAREA"===e.tagName||e.isContentEditable)return!1}catch(t){}return!0}(n))return;const r="keypress"===n.type?"input":n.type;(void 0===Rt||function(t,e){if(!t)return!0;if(t.type!==e.type)return!0;try{if(t.target!==e.target)return!0}catch(t){}return!1}(kt,n))&&(t({event:n,name:r,global:e}),kt=n),clearTimeout(Rt),Rt=mt.a.setTimeout(()=>{Rt=void 0},1e3)}}let Ut=null;let Bt=null;function It(t,e){const n=Gt(t,e),r={type:e&&e.name,value:Pt(e)};return n.length&&(r.stacktrace={frames:n}),void 0===r.type&&""===r.value&&(r.value="Unrecoverable error caught"),r}function Yt(t,e){return{exception:{values:[It(t,e)]}}}function Gt(t,e){const n=e.stacktrace||e.stack||"",r=function(t){if(t){if("number"==typeof t.framesToPop)return t.framesToPop;if(Ct.test(t.message))return 1}return 0}(e);try{return t(n,r)}catch(t){}return[]}const Ct=/Minified React error #\d+;/i;function Pt(t){const e=t&&t.message;return e?e.error&&"string"==typeof e.error.message?e.error.message:e:"No error message"}function Lt(t,e,n,r,i){let s;if(Object(j.e)(e)&&e.error){return Yt(t,e.error)}if(Object(j.a)(e)||Object(j.b)(e)){const i=e;if("stack"in e)s=Yt(t,e);else{const e=i.name||(Object(j.a)(i)?"DOMError":"DOMException"),o=i.message?`${e}: ${i.message}`:e;s=$t(t,o,n,r),m(s,o)}return"code"in i&&(s.tags={...s.tags,"DOMException.code":""+i.code}),s}if(Object(j.d)(e))return Yt(t,e);if(Object(j.i)(e)||Object(j.f)(e)){return s=function(t,e,n,r){const i=$().getClient(),s=i&&i.getOptions().normalizeDepth,c={exception:{values:[{type:Object(j.f)(e)?e.constructor.name:r?"UnhandledRejection":"Error",value:`Non-Error ${r?"promise rejection":"exception"} captured with keys: ${Object(o.d)(e)}`}]},extra:{__serialized__:Object(st.b)(e,s)}};if(n){const e=Gt(t,n);e.length&&(c.exception.values[0].stacktrace={frames:e})}return c}(t,e,n,i),y(s,{synthetic:!0}),s}return s=$t(t,e,n,r),m(s,""+e,void 0),y(s,{synthetic:!0}),s}function $t(t,e,n,r){const i={message:e};if(r&&n){const r=Gt(t,n);r.length&&(i.exception={values:[{value:e,stacktrace:{frames:r}}]})}return i}let Mt=0;function At(){return Mt>0}function qt(){Mt+=1,setTimeout(()=>{Mt-=1})}function Ft(t,e={},n){if("function"!=typeof t)return t;try{const e=t.__sentry_wrapped__;if(e)return e;if(Object(o.f)(t))return t}catch(e){return t}const r=function(){const r=Array.prototype.slice.call(arguments);try{n&&"function"==typeof n&&n.apply(this,arguments);const i=r.map(t=>Ft(t,e));return t.apply(this,i)}catch(t){throw qt(),nt(n=>{n.addEventProcessor(t=>(e.mechanism&&(m(t,void 0,void 0),y(t,e.mechanism)),t.extra={...t.extra,arguments:r},t)),H(t)}),t}};try{for(const e in t)Object.prototype.hasOwnProperty.call(t,e)&&(r[e]=t[e])}catch(t){}Object(o.g)(r,t),Object(o.a)(t,"__sentry_wrapped__",r);try{Object.getOwnPropertyDescriptor(r,"name").configurable&&Object.defineProperty(r,"name",{get:()=>t.name})}catch(t){}return r}class Ht{static __initStatic(){this.id="GlobalHandlers"}__init(){this.name=Ht.id}__init2(){this._installFunc={onerror:zt,onunhandledrejection:Wt}}constructor(t){Ht.prototype.__init.call(this),Ht.prototype.__init2.call(this),this._options={onerror:!0,onunhandledrejection:!0,...t}}setupOnce(){Error.stackTraceLimit=50;const t=this._options;for(const n in t){const r=this._installFunc[n];r&&t[n]&&(e=n,("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log("Global Handler attached: "+e),r(),this._installFunc[n]=void 0)}var e}}function zt(){wt("error",t=>{const[e,n,r]=Kt();if(!e.getIntegration(Ht))return;const{msg:i,url:o,line:s,column:c,error:a}=t;if(At()||a&&a.__sentry_own_request__)return;const u=void 0===a&&Object(j.l)(i)?function(t,e,n,r){let i=Object(j.e)(t)?t.message:t,o="Error";const s=i.match(/^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/i);s&&(o=s[1],i=s[2]);return Xt({exception:{values:[{type:o,value:i}]}},e,n,r)}(i,o,s,c):Xt(Lt(n,a||i,void 0,r,!1),o,s,c);u.level="error",Jt(e,a,u,"onerror")})}function Wt(){wt("unhandledrejection",t=>{const[e,n,r]=Kt();if(!e.getIntegration(Ht))return;let i=t;try{"reason"in t?i=t.reason:"detail"in t&&"reason"in t.detail&&(i=t.detail.reason)}catch(t){}if(At()||i&&i.__sentry_own_request__)return!0;const o=Object(j.j)(i)?{exception:{values:[{type:"UnhandledRejection",value:"Non-Error promise rejection captured with value: "+String(i)}]}}:Lt(n,i,void 0,r,!0);o.level="error",Jt(e,i,o,"onunhandledrejection")})}function Xt(t,e,n,r){const i=t.exception=t.exception||{},o=i.values=i.values||[],s=o[0]=o[0]||{},c=s.stacktrace=s.stacktrace||{},a=c.frames=c.frames||[],u=isNaN(parseInt(r,10))?void 0:r,d=isNaN(parseInt(n,10))?void 0:n,f=Object(j.l)(e)&&e.length>0?e:Object(mt.b)();return 0===a.length&&a.push({colno:u,filename:f,function:"?",in_app:!0,lineno:d}),t}function Jt(t,e,n,r){y(n,{handled:!1,type:r}),t.captureEvent(n,{originalException:e})}function Kt(){const t=$(),e=t.getClient(),n=e&&e.getOptions()||{stackParser:()=>[],attachStacktrace:!1};return[t,n.stackParser,n.attachStacktrace]}Ht.__initStatic();const Vt=["EventTarget","Window","Node","ApplicationCache","AudioTrackList","ChannelMergerNode","CryptoOperation","EventSource","FileReader","HTMLUnknownElement","IDBDatabase","IDBRequest","IDBTransaction","KeyOperation","MediaController","MessagePort","ModalWindow","Notification","SVGElementInstance","Screen","TextTrack","TextTrackCue","TextTrackList","WebSocket","WebSocketWorker","Worker","XMLHttpRequest","XMLHttpRequestEventTarget","XMLHttpRequestUpload"];class Qt{static __initStatic(){this.id="TryCatch"}__init(){this.name=Qt.id}constructor(t){Qt.prototype.__init.call(this),this._options={XMLHttpRequest:!0,eventTarget:!0,requestAnimationFrame:!0,setInterval:!0,setTimeout:!0,...t}}setupOnce(){this._options.setTimeout&&Object(o.e)(mt.a,"setTimeout",Zt),this._options.setInterval&&Object(o.e)(mt.a,"setInterval",Zt),this._options.requestAnimationFrame&&Object(o.e)(mt.a,"requestAnimationFrame",te),this._options.XMLHttpRequest&&"XMLHttpRequest"in mt.a&&Object(o.e)(XMLHttpRequest.prototype,"send",ee);const t=this._options.eventTarget;if(t){(Array.isArray(t)?t:Vt).forEach(ne)}}}function Zt(t){return function(...e){const n=e[0];return e[0]=Ft(n,{mechanism:{data:{function:Object(yt.b)(t)},handled:!0,type:"instrument"}}),t.apply(this,e)}}function te(t){return function(e){return t.apply(this,[Ft(e,{mechanism:{data:{function:"requestAnimationFrame",handler:Object(yt.b)(t)},handled:!0,type:"instrument"}})])}}function ee(t){return function(...e){const n=this;return["onload","onerror","onprogress","onreadystatechange"].forEach(t=>{t in n&&"function"==typeof n[t]&&Object(o.e)(n,t,(function(e){const n={mechanism:{data:{function:t,handler:Object(yt.b)(e)},handled:!0,type:"instrument"}},r=Object(o.f)(e);return r&&(n.mechanism.data.handler=Object(yt.b)(r)),Ft(e,n)}))}),t.apply(this,e)}}function ne(t){const e=mt.a,n=e[t]&&e[t].prototype;n&&n.hasOwnProperty&&n.hasOwnProperty("addEventListener")&&(Object(o.e)(n,"addEventListener",(function(e){return function(n,r,i){try{"function"==typeof r.handleEvent&&(r.handleEvent=Ft(r.handleEvent,{mechanism:{data:{function:"handleEvent",handler:Object(yt.b)(r),target:t},handled:!0,type:"instrument"}}))}catch(t){}return e.apply(this,[n,Ft(r,{mechanism:{data:{function:"addEventListener",handler:Object(yt.b)(r),target:t},handled:!0,type:"instrument"}}),i])}})),Object(o.e)(n,"removeEventListener",(function(t){return function(e,n,r){const i=n;try{const n=i&&i.__sentry_wrapped__;n&&t.call(this,e,n,r)}catch(t){}return t.call(this,e,i,r)}})))}Qt.__initStatic();const re=["fatal","error","warning","log","info","debug"];function ie(t){return"warn"===t?"warning":re.includes(t)?t:"log"}function oe(t){if(!t)return{};const e=t.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/);if(!e)return{};const n=e[6]||"",r=e[8]||"";return{host:e[4],path:e[5],protocol:e[2],relative:e[5]+n+r}}class se{static __initStatic(){this.id="Breadcrumbs"}__init(){this.name=se.id}constructor(t){se.prototype.__init.call(this),this.options={console:!0,dom:!0,fetch:!0,history:!0,sentry:!0,xhr:!0,...t}}setupOnce(){var t;this.options.console&&wt("console",ce),this.options.dom&&wt("dom",(t=this.options.dom,function(e){let n,r="object"==typeof t?t.serializeAttribute:void 0;"string"==typeof r&&(r=[r]);try{n=e.event.target?Object(mt.c)(e.event.target,r):Object(mt.c)(e.event,r)}catch(t){n=""}0!==n.length&&$().addBreadcrumb({category:"ui."+e.name,message:n},{event:e.event,name:e.name,global:e.global})})),this.options.xhr&&wt("xhr",ae),this.options.fetch&&wt("fetch",ue),this.options.history&&wt("history",de)}}function ce(t){const e={category:"console",data:{arguments:t.args,logger:"console"},level:ie(t.level),message:Object(l.b)(t.args," ")};if("assert"===t.level){if(!1!==t.args[0])return;e.message="Assertion failed: "+(Object(l.b)(t.args.slice(1)," ")||"console.assert"),e.data.arguments=t.args.slice(1)}$().addBreadcrumb(e,{input:t.args,level:t.level})}function ae(t){if(t.endTimestamp){if(t.xhr.__sentry_own_request__)return;const{method:e,url:n,status_code:r,body:i}=t.xhr.__sentry_xhr__||{};$().addBreadcrumb({category:"xhr",data:{method:e,url:n,status_code:r},type:"http"},{xhr:t.xhr,input:i})}else;}function ue(t){t.endTimestamp&&(t.fetchData.url.match(/sentry_key/)&&"POST"===t.fetchData.method||(t.error?$().addBreadcrumb({category:"fetch",data:t.fetchData,level:"error",type:"http"},{data:t.error,input:t.args}):$().addBreadcrumb({category:"fetch",data:{...t.fetchData,status_code:t.response.status},type:"http"},{input:t.args,response:t.response})))}function de(t){let e=t.from,n=t.to;const r=oe(mt.a.location.href);let i=oe(e);const o=oe(n);i.path||(i=r),r.protocol===o.protocol&&r.host===o.host&&(n=o.relative),r.protocol===i.protocol&&r.host===i.host&&(e=i.relative),$().addBreadcrumb({category:"navigation",data:{from:e,to:n}})}se.__initStatic();class fe{static __initStatic(){this.id="LinkedErrors"}__init(){this.name=fe.id}constructor(t={}){fe.prototype.__init.call(this),this._key=t.key||"cause",this._limit=t.limit||5}setupOnce(){const t=$().getClient();t&&Y((e,n)=>{const r=$().getIntegration(fe);return r?function(t,e,n,r,i){if(!(r.exception&&r.exception.values&&i&&Object(j.g)(i.originalException,Error)))return r;const o=function t(e,n,r,i,o=[]){if(!Object(j.g)(r[i],Error)||o.length+1>=n)return o;const s=It(e,r[i]);return t(e,n,r[i],i,[s,...o])}(t,n,i.originalException,e);return r.exception.values=[...o,...r.exception.values],r}(t.getOptions().stackParser,r._key,r._limit,e,n):e})}}fe.__initStatic();class _e{constructor(){_e.prototype.__init.call(this)}static __initStatic(){this.id="HttpContext"}__init(){this.name=_e.id}setupOnce(){Y(t=>{if($().getIntegration(_e)){if(!mt.a.navigator&&!mt.a.location&&!mt.a.document)return t;const e=t.request&&t.request.url||mt.a.location&&mt.a.location.href,{referrer:n}=mt.a.document||{},{userAgent:r}=mt.a.navigator||{},i={...e&&{url:e},headers:{...t.request&&t.request.headers,...n&&{Referer:n},...r&&{"User-Agent":r}}};return{...t,request:i}}return t})}}_e.__initStatic();class le{constructor(){le.prototype.__init.call(this)}static __initStatic(){this.id="Dedupe"}__init(){this.name=le.id}setupOnce(t,e){const n=t=>{const n=e().getIntegration(le);if(n){try{if(function(t,e){if(!e)return!1;if(function(t,e){const n=t.message,r=e.message;if(!n&&!r)return!1;if(n&&!r||!n&&r)return!1;if(n!==r)return!1;if(!he(t,e))return!1;if(!pe(t,e))return!1;return!0}(t,e))return!0;if(function(t,e){const n=ge(e),r=ge(t);if(!n||!r)return!1;if(n.type!==r.type||n.value!==r.value)return!1;if(!he(t,e))return!1;if(!pe(t,e))return!1;return!0}(t,e))return!0;return!1}(t,n._previousEvent))return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Event dropped due to being a duplicate of previously captured event."),null}catch(e){return n._previousEvent=t}return n._previousEvent=t}return t};n.id=this.name,t(n)}}function pe(t,e){let n=me(t),r=me(e);if(!n&&!r)return!0;if(n&&!r||!n&&r)return!1;if(n=n,r=r,r.length!==n.length)return!1;for(let t=0;t1&&(a=d.slice(0,-1).join("/"),u=d.pop()),u){const t=u.match(/^\d+/);t&&(u=t[0])}return Ee({host:o,pass:i,path:a,projectId:u,port:s,protocol:n,publicKey:r})}(t):Ee(t);return function(t){if("undefined"!=typeof __SENTRY_DEBUG__&&!__SENTRY_DEBUG__)return;const{port:e,projectId:n,protocol:r}=t;if(["protocol","publicKey","host","projectId"].forEach(e=>{if(!t[e])throw new it(`Invalid Sentry Dsn: ${e} missing`)}),!n.match(/^\d+$/))throw new it("Invalid Sentry Dsn: Invalid projectId "+n);if(!function(t){return"http"===t||"https"===t}(r))throw new it("Invalid Sentry Dsn: Invalid protocol "+r);if(e&&isNaN(parseInt(e,10)))throw new it("Invalid Sentry Dsn: Invalid port "+e)}(e),e}function Se(t){const e=t.protocol?t.protocol+":":"",n=t.port?":"+t.port:"";return`${e}//${t.host}${n}${t.path?"/"+t.path:""}/api/`}function Oe(t,e={}){const n="string"==typeof e?e:e.tunnel,r="string"!=typeof e&&e._metadata?e._metadata.sdk:void 0;return n||`${function(t){return`${Se(t)}${t.projectId}/envelope/`}(t)}?${function(t,e){return Object(o.h)({sentry_key:t.publicKey,sentry_version:"7",...e&&{sentry_client:`${e.name}/${e.version}`}})}(t,r)}`}function we(t){if(!t||!t.sdk)return;const{name:e,version:n}=t.sdk;return{name:e,version:n}}function xe(t,e,n,r){const i=we(n),s=t.type||"event";!function(t,e){e&&(t.sdk=t.sdk||{},t.sdk.name=t.sdk.name||e.name,t.sdk.version=t.sdk.version||e.version,t.sdk.integrations=[...t.sdk.integrations||[],...e.integrations||[]],t.sdk.packages=[...t.sdk.packages||[],...e.packages||[]])}(t,n&&n.sdk);const c=function(t,e,n,r){const i=t.sdkProcessingMetadata&&t.sdkProcessingMetadata.dynamicSamplingContext;return{event_id:t.event_id,sent_at:(new Date).toISOString(),...e&&{sdk:e},...!!n&&{dsn:ve(r)},..."transaction"===t.type&&i&&{trace:Object(o.c)({...i})}}}(t,i,r,e);delete t.sdkProcessingMetadata;return ct(c,[[{type:s},t]])}const Te=[];function je(t){const e=t.defaultIntegrations||[],n=t.integrations;let r;e.forEach(t=>{t.isDefaultInstance=!0}),r=Array.isArray(n)?[...e,...n]:"function"==typeof n?E(n(e)):e;const i=function(t){const e={};return t.forEach(t=>{const{name:n}=t,r=e[n];r&&!r.isDefaultInstance&&t.isDefaultInstance||(e[n]=t)}),Object.values(e)}(r),o=i.findIndex(t=>"Debug"===t.name);if(-1!==o){const[t]=i.splice(o,1);i.push(t)}return i}const De="Not capturing exception because it's already been captured.";class Re{__init(){this._integrations={}}__init2(){this._integrationsInitialized=!1}__init3(){this._numProcessing=0}__init4(){this._outcomes={}}constructor(t){if(Re.prototype.__init.call(this),Re.prototype.__init2.call(this),Re.prototype.__init3.call(this),Re.prototype.__init4.call(this),this._options=t,t.dsn){this._dsn=be(t.dsn);const e=Oe(this._dsn,t);this._transport=t.transport({recordDroppedEvent:this.recordDroppedEvent.bind(this),...t.transportOptions,url:e})}else("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("No DSN provided, client will not do anything.")}captureException(t,e,n){if(v(t))return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log(De));let r=e&&e.event_id;return this._process(this.eventFromException(t,e).then(t=>this._captureEvent(t,e,n)).then(t=>{r=t})),r}captureMessage(t,e,n,r){let i=n&&n.event_id;const o=Object(j.j)(t)?this.eventFromMessage(String(t),e,n):this.eventFromException(t,n);return this._process(o.then(t=>this._captureEvent(t,n,r)).then(t=>{i=t})),i}captureEvent(t,e,n){if(e&&e.originalException&&v(e.originalException))return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log(De));let r=e&&e.event_id;return this._process(this._captureEvent(t,e,n).then(t=>{r=t})),r}captureSession(t){this._isEnabled()?"string"!=typeof t.release?("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Discarded session because of missing or non-string release"):(this.sendSession(t),U(t,{init:!1})):("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("SDK not enabled, will not capture session.")}getDsn(){return this._dsn}getOptions(){return this._options}getTransport(){return this._transport}flush(t){const e=this._transport;return e?this._isClientDoneProcessing(t).then(n=>e.flush(t).then(t=>n&&t)):D(!0)}close(t){return this.flush(t).then(t=>(this.getOptions().enabled=!1,t))}setupIntegrations(){this._isEnabled()&&!this._integrationsInitialized&&(this._integrations=function(t){const e={};return t.forEach(t=>{e[t.name]=t,-1===Te.indexOf(t.name)&&(t.setupOnce(Y,$),Te.push(t.name),("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log("Integration installed: "+t.name))}),e}(this._options.integrations),this._integrationsInitialized=!0)}getIntegrationById(t){return this._integrations[t]}getIntegration(t){try{return this._integrations[t.id]||null}catch(e){return("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn(`Cannot retrieve integration ${t.id} from the current Client`),null}}sendEvent(t,e={}){if(this._dsn){let n=xe(t,this._dsn,this._options._metadata,this._options.tunnel);for(const t of e.attachments||[])n=at(n,_t(t,this._options.transportOptions&&this._options.transportOptions.textEncoder));this._sendEnvelope(n)}}sendSession(t){if(this._dsn){const e=function(t,e,n,r){const i=we(n);return ct({sent_at:(new Date).toISOString(),...i&&{sdk:i},...!!r&&{dsn:ve(e)}},["aggregates"in t?[{type:"sessions"},t]:[{type:"session"},t]])}(t,this._dsn,this._options._metadata,this._options.tunnel);this._sendEnvelope(e)}}recordDroppedEvent(t,e){if(this._options.sendClientReports){const n=`${t}:${e}`;("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log(`Adding outcome: "${n}"`),this._outcomes[n]=this._outcomes[n]+1||1}}_updateSessionFromEvent(t,e){let n=!1,r=!1;const i=e.exception&&e.exception.values;if(i){r=!0;for(const t of i){const e=t.mechanism;if(e&&!1===e.handled){n=!0;break}}}const o="ok"===t.status;(o&&0===t.errors||o&&n)&&(U(t,{...n&&{status:"crashed"},errors:t.errors||Number(r||n)}),this.captureSession(t))}_isClientDoneProcessing(t){return new k(e=>{let n=0;const r=setInterval(()=>{0==this._numProcessing?(clearInterval(r),e(!0)):(n+=1,t&&n>=t&&(clearInterval(r),e(!1)))},1)})}_isEnabled(){return!1!==this.getOptions().enabled&&void 0!==this._dsn}_prepareEvent(t,e,n){const{normalizeDepth:r=3,normalizeMaxBreadth:i=1e3}=this.getOptions(),o={...t,event_id:t.event_id||e.event_id||p(),timestamp:t.timestamp||Object(x.a)()};this._applyClientOptions(o),this._applyIntegrationsMetadata(o);let s=n;e.captureContext&&(s=B.clone(s).update(e.captureContext));let c=D(o);if(s){const t=[...e.attachments||[],...s.getAttachments()];t.length&&(e.attachments=t),c=s.applyToEvent(o,e)}return c.then(t=>"number"==typeof r&&r>0?this._normalizeEvent(t,r,i):t)}_normalizeEvent(t,e,n){if(!t)return null;const r={...t,...t.breadcrumbs&&{breadcrumbs:t.breadcrumbs.map(t=>({...t,...t.data&&{data:Object(st.a)(t.data,e,n)}}))},...t.user&&{user:Object(st.a)(t.user,e,n)},...t.contexts&&{contexts:Object(st.a)(t.contexts,e,n)},...t.extra&&{extra:Object(st.a)(t.extra,e,n)}};return t.contexts&&t.contexts.trace&&r.contexts&&(r.contexts.trace=t.contexts.trace,t.contexts.trace.data&&(r.contexts.trace.data=Object(st.a)(t.contexts.trace.data,e,n))),t.spans&&(r.spans=t.spans.map(t=>(t.data&&(t.data=Object(st.a)(t.data,e,n)),t))),r}_applyClientOptions(t){const e=this.getOptions(),{environment:n,release:r,dist:i,maxValueLength:o=250}=e;"environment"in t||(t.environment="environment"in e?n:"production"),void 0===t.release&&void 0!==r&&(t.release=r),void 0===t.dist&&void 0!==i&&(t.dist=i),t.message&&(t.message=Object(l.d)(t.message,o));const s=t.exception&&t.exception.values&&t.exception.values[0];s&&s.value&&(s.value=Object(l.d)(s.value,o));const c=t.request;c&&c.url&&(c.url=Object(l.d)(c.url,o))}_applyIntegrationsMetadata(t){const e=Object.keys(this._integrations);e.length>0&&(t.sdk=t.sdk||{},t.sdk.integrations=[...t.sdk.integrations||[],...e])}_captureEvent(t,e={},n){return this._processEvent(t,e,n).then(t=>t.event_id,t=>{if("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__){const e=t;"log"===e.logLevel?_.log(e.message):_.warn(e)}})}_processEvent(t,e,n){const{beforeSend:r,sampleRate:i}=this.getOptions();if(!this._isEnabled())return R(new it("SDK not enabled, will not capture event.","log"));const o="transaction"===t.type;return!o&&"number"==typeof i&&Math.random()>i?(this.recordDroppedEvent("sample_rate","error"),R(new it(`Discarding event because it's not included in the random sample (sampling rate = ${i})`,"log"))):this._prepareEvent(t,e,n).then(n=>{if(null===n)throw this.recordDroppedEvent("event_processor",t.type||"error"),new it("An event processor returned null, will not send event.","log");if(e.data&&!0===e.data.__sentry__||o||!r)return n;return function(t){const e="`beforeSend` method has to return `null` or a valid event.";if(Object(j.n)(t))return t.then(t=>{if(!Object(j.i)(t)&&null!==t)throw new it(e);return t},t=>{throw new it("beforeSend rejected with "+t)});if(!Object(j.i)(t)&&null!==t)throw new it(e);return t}(r(n,e))}).then(r=>{if(null===r)throw this.recordDroppedEvent("before_send",t.type||"error"),new it("`beforeSend` returned `null`, will not send event.","log");const i=n&&n.getSession();!o&&i&&this._updateSessionFromEvent(i,r);const s=r.transaction_info;if(o&&s&&r.transaction!==t.transaction){const t="custom";r.transaction_info={...s,source:t,changes:[...s.changes,{source:t,timestamp:r.timestamp,propagations:s.propagations}]}}return this.sendEvent(r,e),r}).then(null,t=>{if(t instanceof it)throw t;throw this.captureException(t,{data:{__sentry__:!0},originalException:t}),new it("Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: "+t)})}_process(t){this._numProcessing+=1,t.then(t=>(this._numProcessing-=1,t),t=>(this._numProcessing-=1,t))}_sendEnvelope(t){this._transport&&this._dsn?this._transport.send(t).then(null,t=>{("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Error while sending event:",t)}):("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Transport disabled")}_clearOutcomes(){const t=this._outcomes;return this._outcomes={},Object.keys(t).map(e=>{const[n,r]=e.split(":");return{reason:n,category:r,quantity:t[e]}})}}class ke extends Re{constructor(t){t._metadata=t._metadata||{},t._metadata.sdk=t._metadata.sdk||{name:"sentry.javascript.browser",packages:[{name:"npm:@sentry/browser",version:F}],version:F},super(t),t.sendClientReports&&mt.a.document&&mt.a.document.addEventListener("visibilitychange",()=>{"hidden"===mt.a.document.visibilityState&&this._flushOutcomes()})}eventFromException(t,e){return function(t,e,n,r){const i=Lt(t,e,n&&n.syntheticException||void 0,r);return y(i),i.level="error",n&&n.event_id&&(i.event_id=n.event_id),D(i)}(this._options.stackParser,t,e,this._options.attachStacktrace)}eventFromMessage(t,e="info",n){return function(t,e,n="info",r,i){const o=$t(t,e,r&&r.syntheticException||void 0,i);return o.level=n,r&&r.event_id&&(o.event_id=r.event_id),D(o)}(this._options.stackParser,t,e,n,this._options.attachStacktrace)}sendEvent(t,e){const n=this.getIntegrationById("Breadcrumbs");n&&n.options&&n.options.sentry&&$().addBreadcrumb({category:"sentry."+("transaction"===t.type?"transaction":"event"),event_id:t.event_id,level:t.level,message:g(t)},{event:t}),super.sendEvent(t,e)}_prepareEvent(t,e,n){return t.platform=t.platform||"javascript",super._prepareEvent(t,e,n)}_flushOutcomes(){const t=this._clearOutcomes();if(0===t.length)return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log("No outcomes to send"));if(!this._dsn)return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log("No dsn provided, will not send outcomes"));("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.log("Sending outcomes:",t);const e=Oe(this._dsn,this._options),n=(r=t,ct((i=this._options.tunnel&&ve(this._dsn))?{dsn:i}:{},[[{type:"client_report"},{timestamp:o||Object(x.a)(),discarded_events:r}]]));var r,i,o;try{const t="[object Navigator]"===Object.prototype.toString.call(mt.a&&mt.a.navigator);if(t&&"function"==typeof mt.a.navigator.sendBeacon&&!this._options.transportOptions){mt.a.navigator.sendBeacon.bind(mt.a.navigator)(e,ft(n))}else this._sendEnvelope(n)}catch(t){("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error(t)}}}let Ne;function Ue(t,e=function(){if(Ne)return Ne;if(Et(mt.a.fetch))return Ne=mt.a.fetch.bind(mt.a);const t=mt.a.document;let e=mt.a.fetch;if(t&&"function"==typeof t.createElement)try{const n=t.createElement("iframe");n.hidden=!0,t.head.appendChild(n);const r=n.contentWindow;r&&r.fetch&&(e=r.fetch),t.head.removeChild(n)}catch(t){("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ",t)}return Ne=e.bind(mt.a)}()){return gt(t,(function(n){const r={body:n.body,method:"POST",referrerPolicy:"origin",headers:t.headers,keepalive:n.body.length<=65536,...t.fetchOptions};return e(t.url,r).then(t=>({statusCode:t.status,headers:{"x-sentry-rate-limits":t.headers.get("X-Sentry-Rate-Limits"),"retry-after":t.headers.get("Retry-After")}}))}))}function Be(t){return gt(t,(function(e){return new k((n,r)=>{const i=new XMLHttpRequest;i.onerror=r,i.onreadystatechange=()=>{4===i.readyState&&n({statusCode:i.status,headers:{"x-sentry-rate-limits":i.getResponseHeader("X-Sentry-Rate-Limits"),"retry-after":i.getResponseHeader("Retry-After")}})},i.open("POST",t.url);for(const e in t.headers)Object.prototype.hasOwnProperty.call(t.headers,e)&&i.setRequestHeader(e,t.headers[e]);i.send(e.body)})}))}function Ie(t,e,n,r){const i={filename:t,function:e,in_app:!0};return void 0!==n&&(i.lineno=n),void 0!==r&&(i.colno=r),i}const Ye=/^\s*at (?:(.*\).*?|.*?) ?\((?:address at )?)?((?:file|https?|blob|chrome-extension|address|native|eval|webpack||[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,Ge=/\((\S*)(?::(\d+))(?::(\d+))\)/,Ce=[30,t=>{const e=Ye.exec(t);if(e){if(e[2]&&0===e[2].indexOf("eval")){const t=Ge.exec(e[2]);t&&(e[2]=t[1],e[3]=t[2],e[4]=t[3])}const[t,n]=Je(e[1]||"?",e[2]);return Ie(n,t,e[3]?+e[3]:void 0,e[4]?+e[4]:void 0)}}],Pe=/^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|safari-extension|safari-web-extension|capacitor)?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i,Le=/(\S+) line (\d+)(?: > eval line \d+)* > eval/i,$e=[50,t=>{const e=Pe.exec(t);if(e){if(e[3]&&e[3].indexOf(" > eval")>-1){const t=Le.exec(e[3]);t&&(e[1]=e[1]||"eval",e[3]=t[1],e[4]=t[2],e[5]="")}let t=e[3],n=e[1]||"?";return[n,t]=Je(n,t),Ie(t,n,e[4]?+e[4]:void 0,e[5]?+e[5]:void 0)}}],Me=/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,Ae=[40,t=>{const e=Me.exec(t);return e?Ie(e[2],e[1]||"?",+e[3],e[4]?+e[4]:void 0):void 0}],qe=/ line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i,Fe=[10,t=>{const e=qe.exec(t);return e?Ie(e[2],e[3]||"?",+e[1]):void 0}],He=/ line (\d+), column (\d+)\s*(?:in (?:]+)>|([^)]+))\(.*\))? in (.*):\s*$/i,ze=[20,t=>{const e=He.exec(t);return e?Ie(e[5],e[3]||e[4]||"?",+e[1],+e[2]):void 0}],We=[Ce,$e,Ae],Xe=Object(yt.a)(...We),Je=(t,e)=>{const n=-1!==t.indexOf("safari-extension"),r=-1!==t.indexOf("safari-web-extension");return n||r?[-1!==t.indexOf("@")?t.split("@")[0]:"?",n?"safari-extension:"+e:"safari-web-extension:"+e]:[t,e]};const Ke=[new r.InboundFilters,new r.FunctionToString,new Qt,new se,new Ht,new fe,new le,new _e];function Ve(t={}){void 0===t.defaultIntegrations&&(t.defaultIntegrations=Ke),void 0===t.release&&mt.a.SENTRY_RELEASE&&mt.a.SENTRY_RELEASE.id&&(t.release=mt.a.SENTRY_RELEASE.id),void 0===t.autoSessionTracking&&(t.autoSessionTracking=!0),void 0===t.sendClientReports&&(t.sendClientReports=!0);const e={...t,stackParser:Object(yt.c)(t.stackParser||Xe),integrations:je(t),transport:t.transport||(vt()?Ue:Be)};!function(t,e){!0===e.debug&&("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__?_.enable():console.warn("[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle."));const n=$(),r=n.getScope();r&&r.update(e.initialScope);const i=new t(e);n.bindClient(i)}(ke,e),t.autoSessionTracking&&function(){if(void 0===mt.a.document)return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Session tracking in non-browser environment with @sentry/browser is not supported."));const t=$();if(!t.captureSession)return;sn(t),wt("history",({from:t,to:e})=>{void 0!==t&&t!==e&&sn($())})}()}function Qe(t={},e=$()){if(!mt.a.document)return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Global document not defined in showReportDialog call"));const{client:n,scope:r}=e.getStackTop(),i=t.dsn||n&&n.getDsn();if(!i)return void(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("DSN not configured for showReportDialog call"));r&&(t.user={...r.getUser(),...t.user}),t.eventId||(t.eventId=e.lastEventId());const o=mt.a.document.createElement("script");o.async=!0,o.src=function(t,e){const n=be(t),r=Se(n)+"embed/error-page/";let i="dsn="+ve(n);for(const t in e)if("dsn"!==t)if("user"===t){const t=e.user;if(!t)continue;t.name&&(i+="&name="+encodeURIComponent(t.name)),t.email&&(i+="&email="+encodeURIComponent(t.email))}else i+=`&${encodeURIComponent(t)}=${encodeURIComponent(e[t])}`;return`${r}?${i}`}(i,t),t.onLoad&&(o.onload=t.onLoad);const s=mt.a.document.head||mt.a.document.body;s?s.appendChild(o):("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.error("Not injecting report dialog. No injection point found in HTML")}function Ze(){return $().lastEventId()}function tn(){}function en(t){t()}function nn(t){const e=$().getClient();return e?e.flush(t):(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Cannot flush events. No client defined."),D(!1))}function rn(t){const e=$().getClient();return e?e.close(t):(("undefined"==typeof __SENTRY_DEBUG__||__SENTRY_DEBUG__)&&_.warn("Cannot flush events and disable SDK. No client defined."),D(!1))}function on(t){return Ft(t)()}function sn(t){t.startSession({ignoreDuration:!0}),t.captureSession()}let cn={};mt.a.Sentry&&mt.a.Sentry.Integrations&&(cn=mt.a.Sentry.Integrations);const an={...cn,...r,...i}}]); -------------------------------------------------------------------------------- /cartridges/plugin_sentry/cartridge/templates/default/sentry/tag.isml: -------------------------------------------------------------------------------- 1 | 2 | var assets = require('*/cartridge/scripts/assets.js'); 3 | assets.addJs('/js/sentry.js'); 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /cartridges/plugin_sentry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": "./cartridge/scripts/hooks.json" 3 | } 4 | -------------------------------------------------------------------------------- /ismllinter.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Please check all available configurations and rules 4 | // at https://www.npmjs.com/package/isml-linter. 5 | 6 | var config = { 7 | enableCache: true, 8 | rules: { 9 | 'no-space-only-lines': {}, 10 | 'no-tabs': {}, 11 | 'no-trailing-spaces': {} 12 | } 13 | }; 14 | 15 | module.exports = config; 16 | -------------------------------------------------------------------------------- /metadata/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | qewlDlC9qHTiw1D1Lx+iT8Tj5xfp2HCQEhM8pbliHeM= 7 | 8 | 9 | 10 | 500 11 | false 12 | 0 13 | 0 14 | false 15 | 0 16 | 0 17 | 18 | 19 | 20 | HTTP 21 | true 22 | sentry 23 | false 24 | true 25 | false 26 | Sentry 27 | Sentry 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /metadata/site-preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Public Key (DSN) 7 | The public key to send API events too from the Storefront. 8 | string 9 | false 10 | false 11 | 0 12 | https://.+\.ingest\.sentry\.io/\d+ 13 | 14 | 15 | Project ID (Name) 16 | The unique ID used to identify this project . 17 | string 18 | false 19 | false 20 | 0 21 | 22 | 23 | Cookie Tracking Enabled 24 | Whether or not cookies should be sent to Sentry. 25 | boolean 26 | false 27 | false 28 | true 29 | 30 | 31 | 32 | 33 | Sentry Configuration 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin_sentry", 3 | "version": "6.2.0", 4 | "description": "Track SFRA client side exceptions to Sentry, and log server side exceptions optionally.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "sgmf-scripts --test test/unit/**/*.js", 8 | "lint": "sgmf-scripts --lint js", 9 | "lint:isml": "./node_modules/.bin/isml-linter", 10 | "upload": "sgmf-scripts --upload -- ", 11 | "uploadCartridge": "sgmf-scripts --uploadCartridge lib_sentry && sgmf-scripts --uploadCartridge plugin_sentry", 12 | "compile:js": "sgmf-scripts --compile js", 13 | "build": "npm run compile:js", 14 | "watch": "sgmf-scripts --watch", 15 | "prepare": "husky install" 16 | }, 17 | "repository": { 18 | "url": "https://github.com/taurgis/link_sentry" 19 | }, 20 | "license": "MIT License", 21 | "author": { 22 | "email": "thomas.theunen@forward.eu", 23 | "name": "Thomas Theunen", 24 | "url": "https://www.thomastheunen.eu" 25 | }, 26 | "devDependencies": { 27 | "app-module-path": "2.2.0", 28 | "chai": "4.3.6", 29 | "chai-subset": "1.6.0", 30 | "eslint": "8.26.0", 31 | "eslint-config-airbnb-base": "15.0.0", 32 | "eslint-plugin-import": "2.26.0", 33 | "eslint-plugin-sitegenesis": "1.0.0", 34 | "husky": "8.0.1", 35 | "isml-linter": "5.40.3", 36 | "mocha": "10.1.0", 37 | "proxyquire": "2.1.3", 38 | "sgmf-scripts": "2.4.2", 39 | "sinon": "14.0.1" 40 | }, 41 | "browserslist": [ 42 | "last 2 versions", 43 | "ie >= 10" 44 | ], 45 | "paths": { 46 | "base": "../storefront-reference-architecture/cartridges/app_storefront_base/" 47 | }, 48 | "dependencies": { 49 | "@sentry/browser": "7.16.0", 50 | "@sentry/tracing": "7.16.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/unit/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 2017 7 | }, 8 | "rules": { 9 | "require-jsdoc": "off", 10 | "max-len": "off", 11 | "quote-props": "off", 12 | "no-new": "off", 13 | "valid-jsdoc": "off", 14 | "no-unused-expressions": "off", 15 | "no-proto": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/models/SentryBreadcrumb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | 6 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 7 | 8 | var SentryBreadcrumb = proxyQuire('lib_sentry/cartridge/models/SentryBreadcrumb', { 9 | '*/cartridge/scripts/util/collections': { 10 | forEach: (collection, callback) => { 11 | collection.forEach(callback); 12 | } 13 | } 14 | }); 15 | 16 | describe('Model - Sentry Breadcrumb', () => { 17 | it('Should be empty if no parameters are passed.', () => { 18 | const result = new SentryBreadcrumb(null); 19 | 20 | expect(result).to.be.be.empty; 21 | }); 22 | 23 | it('Should not set breadcrumbs when there is no click stream.', () => { 24 | var dummyRequest = { 25 | session: { 26 | clickStream: { 27 | enabled: false 28 | } 29 | } 30 | }; 31 | 32 | const result = new SentryBreadcrumb(dummyRequest); 33 | 34 | expect(result.values).to.be.be.length(0); 35 | }); 36 | 37 | it('Should set breadcrumbs when there is a click stream', () => { 38 | var dummyRequest = { 39 | session: { 40 | clickStream: { 41 | enabled: true, 42 | clicks: [{ 43 | url: 'my-url', 44 | timestamp: Date.now() 45 | }] 46 | } 47 | } 48 | }; 49 | 50 | const result = new SentryBreadcrumb(dummyRequest); 51 | 52 | expect(result.values).to.be.be.length(1); 53 | expect(result.values).to.deep.equal([{ 54 | data: { 55 | from: null, 56 | to: 'my-url' 57 | }, 58 | type: 'navigation', 59 | timestamp: Math.round(Date.now() / 1000) 60 | }]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/models/SentryEvent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | const sinon = require('sinon'); 6 | 7 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 8 | 9 | const breadcrumbsStub = sinon.stub(); 10 | const userStub = sinon.stub(); 11 | const requestStub = sinon.stub(); 12 | 13 | const SentryEvent = proxyQuire('lib_sentry/cartridge/models/SentryEvent', { 14 | 'dw/system/System': { 15 | getInstanceType: () => 0 16 | }, 17 | '*/cartridge/models/SentryId': proxyQuire('lib_sentry/cartridge/models/SentryId', { 18 | 'dw/util/UUIDUtils': { 19 | createUUID: () => 'xxxxxxxxxxxxxXxxxxxxxxxxxx' 20 | } 21 | }), 22 | '*/cartridge/models/SentryUser': userStub, 23 | '*/cartridge/models/SentryRequest': requestStub, 24 | '*/cartridge/models/SentryBreadcrumb': breadcrumbsStub 25 | }); 26 | 27 | describe('Model - Sentry Event', () => { 28 | before(() => { 29 | global.request = { 30 | httpPath: 'httpPath', 31 | httpHost: 'httpHost', 32 | httpMethod: 'POST', 33 | httpQueryString: 'test=value', 34 | httpRemoteAddress: '127.0.0°.1', 35 | httpHeaders: { }, 36 | httpURL: 'httpURL', 37 | httpCookies: [{ 38 | name: 'cookie1', 39 | value: 'value1' 40 | }, { 41 | name: 'cookie2', 42 | value: 'value2' 43 | }], 44 | session: { 45 | customer: {} 46 | } 47 | }; 48 | 49 | global.request.httpCookies.cookieCount = 2; 50 | }); 51 | 52 | beforeEach(() => { 53 | breadcrumbsStub.reset(); 54 | userStub.reset(); 55 | requestStub.reset(); 56 | }); 57 | 58 | it('Should create an empty object if no parameter is passed', () => { 59 | const result = new SentryEvent(); 60 | 61 | expect(result).to.be.empty; 62 | }); 63 | 64 | it('Should create an empty object if no message is passed', () => { 65 | const result = new SentryEvent({ 66 | release: 'project@version1' 67 | }); 68 | 69 | expect(result).to.be.empty; 70 | }); 71 | 72 | it('Should create an empty object if no release is passed', () => { 73 | const result = new SentryEvent({ 74 | message: 'My message' 75 | }); 76 | 77 | expect(result).to.be.empty; 78 | }); 79 | 80 | it('Should contain an automatic generated UUID', () => { 81 | const result = new SentryEvent({ 82 | error: new Error('My message'), 83 | release: 'project@version1' 84 | }); 85 | 86 | expect(result.event_id).to.equal('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); 87 | }); 88 | 89 | it('Should contain an automatic generated timestamp representing the time now', () => { 90 | const result = new SentryEvent({ 91 | error: new Error('My message'), 92 | release: 'project@version1' 93 | }); 94 | 95 | expect(result.timestamp).to.equal(Math.round(Date.now() / 1000)); 96 | }); 97 | 98 | it('Should contain the platform with the value "javascript"', () => { 99 | const result = new SentryEvent({ 100 | error: new Error('My message'), 101 | release: 'project@version1' 102 | }); 103 | 104 | expect(result.platform).to.equal('javascript'); 105 | }); 106 | 107 | it('Should contain the current request URL on which the exception is created', () => { 108 | const result = new SentryEvent({ 109 | error: new Error('My message'), 110 | release: 'project@version1' 111 | }); 112 | 113 | expect(result.transaction).to.equal(request.httpPath); 114 | }); 115 | 116 | it('Should contain the current request host on which the exception is created', () => { 117 | const result = new SentryEvent({ 118 | error: new Error('My message'), 119 | release: 'project@version1' 120 | }); 121 | 122 | expect(result.server_name).to.equal(request.httpHost); 123 | }); 124 | 125 | it('Should contain the release version based on the Sentry config file', () => { 126 | const projectId = 'project@version1'; 127 | const result = new SentryEvent({ 128 | error: new Error('My message'), 129 | release: projectId 130 | }); 131 | 132 | expect(result.release).to.equal(projectId); 133 | }); 134 | 135 | it('Should contain the current environment', () => { 136 | const result = new SentryEvent({ 137 | error: new Error('My message'), 138 | release: 'project@version1' 139 | }); 140 | 141 | expect(result.environment).to.equal('development'); 142 | }); 143 | 144 | it('Should mark the error as fatal if no level is passed', () => { 145 | const result = new SentryEvent({ 146 | error: new Error('My message'), 147 | release: 'project@version1' 148 | }); 149 | 150 | expect(result.level).to.equal(SentryEvent.LEVEL_FATAL); 151 | }); 152 | 153 | it('Should set the correct error level', () => { 154 | const result = new SentryEvent({ 155 | error: new Error('My message'), 156 | release: 'project@version1', 157 | level: SentryEvent.LEVEL_INFO 158 | }); 159 | 160 | expect(result.level).to.equal(SentryEvent.LEVEL_INFO); 161 | }); 162 | 163 | it('Should set the exception based on the passed parameters', () => { 164 | const error = new Error('My message'); 165 | const result = new SentryEvent({ 166 | error: error, 167 | release: 'project@version1', 168 | type: SentryEvent.ERROR_TYPE_UNKNOWN, 169 | eventType: SentryEvent.TYPE_EXCEPTION 170 | }); 171 | 172 | expect(result.exception).to.deep.equal({ 173 | values: [{ 174 | type: 'Error', 175 | value: error.message + '\n' + error.stack 176 | }] 177 | }); 178 | }); 179 | 180 | it('Should generate breadcrumbs.', () => { 181 | new SentryEvent({ 182 | error: new Error('My message'), 183 | release: 'project@version1', 184 | level: SentryEvent.LEVEL_INFO 185 | }); 186 | 187 | expect(breadcrumbsStub.calledOnce).to.be.true; 188 | expect(breadcrumbsStub.calledWith(request)).to.be.true; 189 | }); 190 | 191 | it('Should generate user info.', () => { 192 | new SentryEvent({ 193 | error: new Error('My message'), 194 | release: 'project@version1', 195 | level: SentryEvent.LEVEL_INFO 196 | }); 197 | 198 | expect(userStub.calledOnce).to.be.true; 199 | expect(userStub.calledWith(request.httpRemoteAddress, request.session.customer)).to.be.true; 200 | }); 201 | 202 | it('Should generate request info.', () => { 203 | new SentryEvent({ 204 | error: new Error('My message'), 205 | release: 'project@version1', 206 | level: SentryEvent.LEVEL_INFO 207 | }); 208 | 209 | expect(requestStub.calledOnce).to.be.true; 210 | expect(requestStub.calledWith(request)).to.be.true; 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/models/SentryId.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | 6 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 7 | 8 | const defaultUUID = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'; 9 | 10 | var SentryId = proxyQuire('lib_sentry/cartridge/models/SentryId', { 11 | 'dw/util/UUIDUtils': { 12 | createUUID: () => defaultUUID 13 | } 14 | }); 15 | 16 | describe('Model - Sentry ID', () => { 17 | it('Should use a generated UUID if none is passed.', () => { 18 | var result = new SentryId(); 19 | 20 | expect(result.uuid).to.equal('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); 21 | }); 22 | 23 | it('Should not allow a short UUID to be used.', () => { 24 | try { 25 | new SentryId(); 26 | } catch (e) { 27 | expect(e.message).to.equal('UUID needs to be 32 characters long.'); 28 | } 29 | }); 30 | 31 | it('Should allow for a custom UUID to be used.', () => { 32 | const customUUID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyz'; 33 | var result = new SentryId(customUUID); 34 | 35 | expect(result.uuid).to.equal(customUUID); 36 | }); 37 | 38 | it('Should return the UUID when using toString.', () => { 39 | var result = new SentryId(); 40 | 41 | expect(result.toString()).to.equal('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/models/SentryOptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | const sinon = require('sinon'); 6 | 7 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 8 | 9 | const defaultDSN = 'xxxxxxxxxxxxxXxxxxxxxxxxxx'; 10 | const defaultProject = 'project'; 11 | const defaultRelease = defaultProject + '@1.0.0'; 12 | const loggerSpy = sinon.spy(); 13 | 14 | var SentryOptions = proxyQuire('lib_sentry/cartridge/models/SentryOptions', { 15 | '*/cartridge/scripts/processors/duplicateEventProcessor': function () { }, 16 | '*/cartridge/scripts/processors/cookieProcessor': function () { }, 17 | 'dw/system/Logger': { 18 | getLogger: loggerSpy 19 | } 20 | }); 21 | 22 | describe('Model - Sentry Options', () => { 23 | it('Should use the DSN provided in the passed config.', () => { 24 | const dsn = 'my_dsn'; 25 | 26 | var result = new SentryOptions({ 27 | dsn, 28 | release: defaultRelease 29 | }); 30 | 31 | expect(result.dsn).to.equal(dsn); 32 | }); 33 | 34 | it('Should use the release provided in the passed config.', () => { 35 | const release = 'my_release'; 36 | 37 | var result = new SentryOptions({ 38 | dsn: defaultDSN, 39 | release 40 | }); 41 | 42 | expect(result.release).to.equal(release); 43 | }); 44 | 45 | it('Should be possible to register a custom event processor.', () => { 46 | var eventProcessor = sinon.spy(); 47 | 48 | var result = new SentryOptions({ 49 | dsn: defaultDSN, 50 | release: defaultRelease 51 | }); 52 | result.addEventProcessor(eventProcessor); 53 | 54 | expect(result.eventProcessors).to.be.length(3); 55 | expect(eventProcessor.calledOnce).to.be.true; 56 | expect(eventProcessor.calledWith(result)).to.be.true; 57 | }); 58 | 59 | it('Should be possible to get all event processors.', () => { 60 | var result = new SentryOptions({ 61 | dsn: defaultDSN, 62 | release: defaultProject 63 | }); 64 | 65 | expect(result.getEventProcessors()).to.be.length(2); 66 | }); 67 | 68 | it('Should be possible to override the logger.', () => { 69 | var result = new SentryOptions({ 70 | dsn: defaultDSN, 71 | release: defaultRelease 72 | }); 73 | var customLogger = { 74 | debug: function () {} 75 | }; 76 | result.setLogger(customLogger); 77 | 78 | expect(result.logger).to.equal(customLogger); 79 | }); 80 | 81 | it('Should not be possible to override the logger with something that is not a logger.', () => { 82 | var result = new SentryOptions({ 83 | dsn: defaultDSN, 84 | release: defaultRelease 85 | }); 86 | var customLogger = {}; 87 | result.setLogger(customLogger); 88 | 89 | expect(result.logger).to.not.equal(customLogger); 90 | }); 91 | 92 | it('Should be possible to get the logger.', () => { 93 | var result = new SentryOptions({ 94 | dsn: defaultDSN, 95 | release: defaultRelease 96 | }); 97 | var customLogger = { 98 | debug: function () {} 99 | }; 100 | result.setLogger(customLogger); 101 | 102 | expect(result.getLogger()).to.equal(customLogger); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/models/SentryRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | 6 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 7 | 8 | var SentryRequest = proxyQuire('lib_sentry/cartridge/models/SentryRequest', { 9 | '*/cartridge/scripts/util/collections': { 10 | forEach: (collection, callback) => { 11 | collection.forEach(callback); 12 | } 13 | } 14 | }); 15 | 16 | describe('Model - Sentry Request', () => { 17 | let dummyRequest = {}; 18 | 19 | beforeEach(() => { 20 | dummyRequest = { 21 | httpMethod: 'POST', 22 | httpURL: 'https://127.0.0.1', 23 | httpQueryString: 'test1=value1&test2=value2', 24 | httpRemoteAddress: '127.0.0.1' 25 | }; 26 | }); 27 | 28 | it('Should create an empty object if no parameter is passed.', () => { 29 | const result = new SentryRequest(); 30 | 31 | expect(result).to.be.empty; 32 | }); 33 | 34 | it('Should set the request method.', () => { 35 | const result = new SentryRequest(dummyRequest); 36 | 37 | expect(result.method).to.equal(dummyRequest.httpMethod); 38 | }); 39 | 40 | it('Should set the request url.', () => { 41 | const result = new SentryRequest(dummyRequest); 42 | 43 | expect(result.url).to.equal(dummyRequest.httpURL); 44 | }); 45 | 46 | it('Should set the request query string.', () => { 47 | const result = new SentryRequest(dummyRequest); 48 | 49 | expect(result.query_string).to.equal(dummyRequest.httpQueryString); 50 | }); 51 | 52 | it('Should set the remote address.', () => { 53 | const result = new SentryRequest(dummyRequest); 54 | 55 | expect(result.env.REMOTE_ADDR).to.equal(dummyRequest.httpRemoteAddress); 56 | }); 57 | 58 | it('Should not set the request headers if they are not present.', () => { 59 | const result = new SentryRequest(dummyRequest); 60 | 61 | expect(result.headers).to.be.empty; 62 | }); 63 | 64 | it('Should set present headers.', () => { 65 | dummyRequest.httpHeaders = { 66 | keySet: () => ['header1', 'header2'], 67 | values: { 68 | header1: 'headervalue1', 69 | header2: 'headervalue2' 70 | }, 71 | get: (key) => dummyRequest.httpHeaders.values[key] 72 | }; 73 | 74 | const result = new SentryRequest(dummyRequest); 75 | 76 | expect(result.headers).to.deep.equal({ 77 | header1: 'headervalue1', 78 | header2: 'headervalue2' 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/models/SentryUser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | 6 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 7 | 8 | var SentryUser = proxyQuire('lib_sentry/cartridge/models/SentryUser', { 9 | '*/cartridge/scripts/util/collections': { 10 | map: (collection, callback) => { 11 | return collection.map(callback); 12 | } 13 | } 14 | }); 15 | 16 | const userIpAddress = '127.0.0.1'; 17 | const dummyCustomer = { 18 | customerGroups: [{ 19 | ID: 'customer_grp1' 20 | }, { 21 | ID: 'customer_grp2' 22 | }] 23 | }; 24 | 25 | describe('Model - Sentry User', () => { 26 | it('Should set the correct user ip address', () => { 27 | const result = new SentryUser(userIpAddress, dummyCustomer); 28 | 29 | expect(result.ip_address).to.equal(userIpAddress); 30 | }); 31 | 32 | it('Should set the correct user customer groups', () => { 33 | const result = new SentryUser(userIpAddress, dummyCustomer); 34 | 35 | expect(result.customer_groups).to.equal( 36 | dummyCustomer.customerGroups.map(((customerGroup) => customerGroup.ID)).join(', ') 37 | ); 38 | }); 39 | 40 | it('Should not set the customer number if the customer is not logged in, on the user', () => { 41 | dummyCustomer.authenticated = false; 42 | 43 | const result = new SentryUser(userIpAddress, dummyCustomer); 44 | 45 | expect(result.id).to.not.exist; 46 | }); 47 | 48 | it('Should set the customer number if the customer is logged in, on the user', () => { 49 | dummyCustomer.authenticated = true; 50 | dummyCustomer.profile = { 51 | customerNo: '1234' 52 | }; 53 | 54 | const result = new SentryUser(userIpAddress, dummyCustomer); 55 | 56 | expect(result.id).to.equal(dummyCustomer.profile.customerNo); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/scripts/processors/basketProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | const sinon = require('sinon'); 6 | 7 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 8 | 9 | const getCurrentBasketStub = sinon.stub(); 10 | const dummySentryOptions = { 11 | logger: { 12 | debug: sinon.stub() 13 | } 14 | }; 15 | 16 | var BasketProcessor = proxyQuire('lib_sentry/cartridge/scripts/processors/basketProcessor', { 17 | 'dw/order/BasketMgr': { 18 | getCurrentBasket: getCurrentBasketStub 19 | }, 20 | '*/cartridge/scripts/util/collections': { 21 | map: (collection, callback) => { 22 | return collection.map(callback); 23 | } 24 | }, 25 | '*/cartridge/models/SentryOptions': {}, 26 | '*/cartridge/models/SentryEvent': {} 27 | }); 28 | 29 | describe('Processor - Basket Processor', () => { 30 | beforeEach(() => { 31 | getCurrentBasketStub.reset(); 32 | dummySentryOptions.logger.debug.reset(); 33 | }); 34 | 35 | it('Should throw an exception if no Sentry Options are passed', () => { 36 | try { 37 | new BasketProcessor(); 38 | } catch (e) { 39 | expect(e.message).to.equal('The sentryOptions is required.'); 40 | } 41 | }); 42 | 43 | it('Should set the Sentry Options', () =>{ 44 | const result = new BasketProcessor(dummySentryOptions); 45 | 46 | expect(result.options).to.equal(dummySentryOptions); 47 | }); 48 | 49 | describe('Process', () => { 50 | const basketProcessor = new BasketProcessor(dummySentryOptions); 51 | let sentryEvent = { 52 | user: {} 53 | }; 54 | 55 | it('Should not set basket information if there is no basket.', () => { 56 | getCurrentBasketStub.returns(null); 57 | const result = basketProcessor.process(sentryEvent); 58 | 59 | expect(result.user.basket_products).to.not.exist; 60 | expect(dummySentryOptions.logger.debug.calledOnce).to.be.false; 61 | }); 62 | 63 | it('Should set basket information if there is a basket.', () => { 64 | const dummyProductLineItems = [{ 65 | productID: '1', 66 | productName: 'Product 1' 67 | }, { 68 | productID: '2', 69 | productName: 'Product 2' 70 | }]; 71 | getCurrentBasketStub.returns({ 72 | productLineItems: dummyProductLineItems 73 | }); 74 | 75 | const result = basketProcessor.process(sentryEvent); 76 | 77 | expect(result.user.basket_products).to.equal( 78 | dummyProductLineItems.map( 79 | (productLineItem) => `${productLineItem.productName} (${productLineItem.productID})` 80 | ).join(', ') 81 | ); 82 | expect(dummySentryOptions.logger.debug.calledOnce).to.be.true; 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/scripts/processors/cookieProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | const sinon = require('sinon'); 6 | 7 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 8 | 9 | const cookiesAllowedStub = sinon.stub(); 10 | const dummySentryOptions = { 11 | logger: { 12 | debug: sinon.stub() 13 | } 14 | }; 15 | 16 | var CookieProcessor = proxyQuire('lib_sentry/cartridge/scripts/processors/cookieProcessor', { 17 | '*/cartridge/scripts/helpers/sentryHelper': { 18 | getCookiesAllowed: cookiesAllowedStub 19 | }, 20 | '*/cartridge/models/SentryOptions': {}, 21 | '*/cartridge/models/SentryEvent': {} 22 | }); 23 | 24 | describe('Processor - Cookie Processor', () => { 25 | beforeEach(() => { 26 | cookiesAllowedStub.reset(); 27 | dummySentryOptions.logger.debug.reset(); 28 | }); 29 | 30 | it('Should throw an exception if no Sentry Options are passed', () => { 31 | try { 32 | new CookieProcessor(); 33 | } catch (e) { 34 | expect(e.message).to.equal('The sentryOptions is required.'); 35 | } 36 | }); 37 | 38 | it('Should set the Sentry Options', () =>{ 39 | const result = new CookieProcessor(dummySentryOptions); 40 | 41 | expect(result.options).to.equal(dummySentryOptions); 42 | }); 43 | 44 | describe('Process', () => { 45 | const cookieProcessor = new CookieProcessor(dummySentryOptions); 46 | let sentryEvent = null; 47 | 48 | before(()=> { 49 | global.request = { 50 | httpCookies: [{ 51 | name: 'header1', 52 | value: 'value1' 53 | }, { 54 | name: 'header2', 55 | value: 'value2' 56 | }] 57 | }; 58 | 59 | global.request.httpCookies.cookieCount = 2; 60 | }); 61 | 62 | beforeEach(() => { 63 | sentryEvent = { 64 | request: {} 65 | }; 66 | }); 67 | 68 | it('Should not set cookies if it is not allowed.', () => { 69 | cookiesAllowedStub.returns(false); 70 | const result = cookieProcessor.process(sentryEvent); 71 | 72 | expect(result.request.cookies).to.not.exist; 73 | expect(dummySentryOptions.logger.debug.calledOnce).to.be.true; 74 | }); 75 | 76 | it('Should set cookies if it is allowed.', () => { 77 | cookiesAllowedStub.returns(true); 78 | const result = cookieProcessor.process(sentryEvent); 79 | 80 | expect(result.request.cookies).to.equal(request.httpCookies.map((cookie) => cookie.name + '=' + cookie.value).join('; ') + '; '); 81 | expect(dummySentryOptions.logger.debug.calledOnce).to.be.true; 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/scripts/processors/duplicateEventProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | const sinon = require('sinon'); 6 | 7 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 8 | 9 | const dummySentryOptions = { 10 | logger: { 11 | debug: sinon.stub() 12 | } 13 | }; 14 | 15 | const getCacheStub = sinon.stub(); 16 | const putCacheStub = sinon.stub(); 17 | const bytesStub = sinon.stub(); 18 | 19 | var DuplicateEventProcessor = proxyQuire('lib_sentry/cartridge/scripts/processors/duplicateEventProcessor', { 20 | '*/cartridge/models/SentryOptions': {}, 21 | '*/cartridge/models/SentryEvent': {}, 22 | 'dw/system/CacheMgr': { 23 | getCache: function () { 24 | return { 25 | get: getCacheStub, 26 | put: putCacheStub 27 | }; 28 | } 29 | }, 30 | 'dw/util/Bytes': bytesStub, 31 | 'dw/crypto/MessageDigest': function () { 32 | this.digestBytes = function () { 33 | return 'DIGEST'; 34 | }; 35 | }, 36 | 'dw/crypto/Encoding': { 37 | toHex: () => 'HEX VALUE' 38 | } 39 | }); 40 | 41 | describe('Processor - Duplicate Event Processor', () => { 42 | beforeEach(() => { 43 | dummySentryOptions.logger.debug.reset(); 44 | getCacheStub.reset(); 45 | putCacheStub.reset(); 46 | }); 47 | 48 | it('Should throw an exception if no Sentry Options are passed', () => { 49 | try { 50 | new DuplicateEventProcessor(); 51 | } catch (e) { 52 | expect(e.message).to.equal('The sentryOptions is required.'); 53 | } 54 | }); 55 | 56 | it('Should set the Sentry Options', () =>{ 57 | const result = new DuplicateEventProcessor(dummySentryOptions); 58 | 59 | expect(result.options).to.equal(dummySentryOptions); 60 | }); 61 | 62 | describe('Process', () => { 63 | let sentryEvent = null; 64 | const duplicateEventProcessor = new DuplicateEventProcessor(dummySentryOptions); 65 | 66 | before(() => { 67 | global.request = { 68 | session: { 69 | sessionID: 'MY_ID' 70 | } 71 | }; 72 | }); 73 | 74 | beforeEach(() => { 75 | sentryEvent = { 76 | error: new Error('dummy error') 77 | }; 78 | }); 79 | 80 | it('Should do nothing if the Sentry Event has no error.', () =>{ 81 | delete sentryEvent.error; 82 | 83 | const result = duplicateEventProcessor.process(sentryEvent); 84 | 85 | expect(getCacheStub.notCalled).to.be.true; 86 | expect(putCacheStub.notCalled).to.be.true; 87 | expect(result).to.equal(sentryEvent); 88 | }); 89 | 90 | it('Should store a event in the cache to prevent duplicate calls for that session.', () =>{ 91 | duplicateEventProcessor.process(sentryEvent); 92 | 93 | expect(getCacheStub.called).to.be.true; 94 | expect(putCacheStub.called).to.be.true; 95 | expect(bytesStub.calledWith( 96 | request.session.sessionID + sentryEvent.error.message + sentryEvent.error.stack 97 | )).to.be.true; 98 | expect(putCacheStub.calledWith('HEX VALUE')).to.be.true; 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/scripts/sentry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | const sinon = require('sinon'); 6 | 7 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 8 | 9 | let Sentry; 10 | const sentryEventStub = sinon.stub(); 11 | const sentryOptionsStub = sinon.stub(); 12 | const eventProcessorsStub = sinon.stub(); 13 | const hasHookStub = sinon.stub(); 14 | const callHookStub = sinon.stub(); 15 | const sendEventStub = sinon.stub(); 16 | 17 | describe('Sentry', () => { 18 | beforeEach(() => { 19 | sentryEventStub.reset(); 20 | sentryOptionsStub.reset(); 21 | eventProcessorsStub.reset(); 22 | hasHookStub.reset(); 23 | callHookStub.reset(); 24 | sendEventStub.reset(); 25 | 26 | Sentry = proxyQuire('lib_sentry/cartridge/scripts/Sentry', { 27 | '*/cartridge/models/SentryEvent': sentryEventStub, 28 | '*/cartridge/models/SentryOptions': sentryOptionsStub, 29 | '*/cartridge/scripts/helpers/sentryHelper': { 30 | getDSN: () => 'DSN', 31 | getProjectName: () => 'ProjectID' 32 | }, 33 | '*/cartridge/config/sentry': require('lib_sentry/cartridge/config/sentry') 34 | }); 35 | }); 36 | 37 | it('Should set initialized to false when calling Sentry.', () => { 38 | expect(Sentry.initialized).to.be.false; 39 | }); 40 | 41 | it('Should set initialized to true when initializing Sentry.', () => { 42 | Sentry.init(); 43 | expect(Sentry.initialized).to.be.true; 44 | }); 45 | 46 | it('Should fall back to default options if no options are passed.', () => { 47 | Sentry.init(); 48 | 49 | expect(sentryOptionsStub.calledOnce).to.be.true; 50 | expect(Sentry.options).to.not.be.null; 51 | expect(Sentry.options).to.not.be.undefined; 52 | }); 53 | 54 | it('Should use the passed options to initialize Sentry.', () => { 55 | const dummyOptions = { 56 | dsn: 'my_dsn' 57 | }; 58 | 59 | Sentry.init(dummyOptions); 60 | 61 | expect(sentryOptionsStub.calledTwice).to.be.true; 62 | expect(sentryOptionsStub.calledWithExactly(dummyOptions)).to.be.true; 63 | }); 64 | 65 | it('Should be able to get the options.', () => { 66 | const dummyOptions = { 67 | dsn: 'my_dsn' 68 | }; 69 | 70 | Sentry.init(dummyOptions); 71 | expect(sentryOptionsStub.calledWithExactly(dummyOptions)).to.be.true; 72 | expect(Sentry.getOptions()).to.not.be.undefined; 73 | expect(Sentry.getOptions()).to.not.be.null; 74 | }); 75 | 76 | it('Should return the default options if Sentry has not been initizalized.', () => { 77 | expect(Sentry.getOptions()).to.not.be.undefined; 78 | expect(Sentry.getOptions()).to.not.be.null; 79 | }); 80 | 81 | describe('SentryEvent - CaptureException', () => { 82 | beforeEach(() => { 83 | Sentry = proxyQuire('lib_sentry/cartridge/scripts/Sentry', { 84 | '*/cartridge/models/SentryEvent': sentryEventStub, 85 | '*/cartridge/models/SentryOptions': function () { 86 | this.getEventProcessors = eventProcessorsStub; 87 | this.release = '5.3.0'; 88 | }, 89 | '*/cartridge/scripts/helpers/sentryHelper': { 90 | getDSN: () => 'DSN', 91 | getProjectName: () => 'ProjectID', 92 | sendEvent: sendEventStub 93 | }, 94 | '*/cartridge/config/sentry': require('lib_sentry/cartridge/config/sentry'), 95 | 'dw/system/HookMgr': { 96 | hasHook: hasHookStub, 97 | callHook: callHookStub 98 | } 99 | }); 100 | }); 101 | 102 | it('Should initialize Sentry with the default options if it has not been initialized.', () => { 103 | eventProcessorsStub.returns([]); 104 | Sentry.captureException(new Error('My Error')); 105 | 106 | expect(Sentry.initialized).to.be.true; 107 | }); 108 | 109 | it('Should create the sentry event based on the error.', () => { 110 | eventProcessorsStub.returns([]); 111 | const dummyError = new Error('My Error'); 112 | 113 | Sentry.captureException(dummyError); 114 | 115 | expect(sentryEventStub.calledWith(sinon.match({ 116 | error: dummyError, 117 | eventType: undefined, // because of the stubbing 118 | release: '5.3.0', 119 | level: undefined // because of the stubbing 120 | }))).to.be.true; 121 | 122 | expect(sendEventStub.calledOnce).to.be.true; 123 | }); 124 | 125 | it('Should process the created event with all registered processors.', () => { 126 | const dummyProcessor = { 127 | process: sinon.stub() 128 | }; 129 | eventProcessorsStub.returns([dummyProcessor]); 130 | 131 | Sentry.captureException(new Error('My Error')); 132 | 133 | expect(dummyProcessor.process.calledOnce).to.be.true; 134 | }); 135 | 136 | it('Should not call the beforeSend hook if no hook has been registered.', () => { 137 | eventProcessorsStub.returns([]); 138 | hasHookStub.returns(false); 139 | 140 | Sentry.captureException(new Error('My Error')); 141 | 142 | expect(hasHookStub.calledOnce).to.be.true; 143 | expect(callHookStub.calledOnce).to.be.false; 144 | }); 145 | 146 | it('Should call the beforeSend hook if a hook has been registered.', () => { 147 | eventProcessorsStub.returns([]); 148 | hasHookStub.returns(true); 149 | 150 | Sentry.captureException(new Error('My Error')); 151 | 152 | expect(hasHookStub.calledOnce).to.be.true; 153 | expect(callHookStub.calledOnce).to.be.true; 154 | }); 155 | }); 156 | 157 | describe('SentryEvent - captureMessage', () => { 158 | beforeEach(() => { 159 | Sentry = proxyQuire('lib_sentry/cartridge/scripts/Sentry', { 160 | '*/cartridge/models/SentryEvent': sentryEventStub, 161 | '*/cartridge/models/SentryOptions': function () { 162 | this.getEventProcessors = eventProcessorsStub; 163 | this.release = '5.3.0'; 164 | }, 165 | '*/cartridge/scripts/helpers/sentryHelper': { 166 | getDSN: () => 'DSN', 167 | getProjectName: () => 'ProjectID', 168 | sendEvent: sendEventStub 169 | }, 170 | '*/cartridge/config/sentry': require('lib_sentry/cartridge/config/sentry'), 171 | 'dw/system/HookMgr': { 172 | hasHook: hasHookStub, 173 | callHook: callHookStub 174 | } 175 | }); 176 | }); 177 | 178 | it('Should initialize Sentry with the default options if it has not been initialized.', () => { 179 | Sentry.captureMessage('My Message'); 180 | 181 | expect(Sentry.initialized).to.be.true; 182 | }); 183 | 184 | it('Should create the sentry event based on the error.', () => { 185 | const dummyMessage = 'My message'; 186 | 187 | Sentry.captureMessage(dummyMessage); 188 | 189 | expect(sentryEventStub.calledWith(sinon.match({ 190 | eventType: undefined, // because of the stubbing 191 | release: '5.3.0', 192 | message: dummyMessage, 193 | level: undefined // because of the stubbing 194 | }))).to.be.true; 195 | 196 | expect(sendEventStub.calledOnce).to.be.true; 197 | }); 198 | }); 199 | 200 | describe('SentryEvent - captureEvent', () => { 201 | beforeEach(() => { 202 | Sentry = proxyQuire('lib_sentry/cartridge/scripts/Sentry', { 203 | '*/cartridge/models/SentryEvent': sentryEventStub, 204 | '*/cartridge/models/SentryOptions': function () { 205 | this.getEventProcessors = eventProcessorsStub; 206 | this.release = '5.3.0'; 207 | }, 208 | '*/cartridge/scripts/helpers/sentryHelper': { 209 | getDSN: () => 'DSN', 210 | getProjectName: () => 'ProjectID', 211 | sendEvent: sendEventStub 212 | }, 213 | '*/cartridge/config/sentry': require('lib_sentry/cartridge/config/sentry'), 214 | 'dw/system/HookMgr': { 215 | hasHook: hasHookStub, 216 | callHook: callHookStub 217 | } 218 | }); 219 | }); 220 | 221 | it('Should initialize Sentry with the default options if it has not been initialized.', () => { 222 | eventProcessorsStub.returns([]); 223 | Sentry.captureEvent(sentryEventStub); 224 | 225 | expect(Sentry.initialized).to.be.true; 226 | }); 227 | 228 | it('Should process the passed event with all registered processors.', () => { 229 | const dummyProcessor = { 230 | process: sinon.stub() 231 | }; 232 | eventProcessorsStub.returns([dummyProcessor]); 233 | 234 | Sentry.captureEvent(sentryEventStub); 235 | 236 | expect(dummyProcessor.process.calledOnce).to.be.true; 237 | }); 238 | 239 | it('Should not call the beforeSend hook if no hook has been registered.', () => { 240 | eventProcessorsStub.returns([]); 241 | hasHookStub.returns(false); 242 | 243 | Sentry.captureEvent(sentryEventStub); 244 | 245 | expect(hasHookStub.calledOnce).to.be.true; 246 | expect(callHookStub.calledOnce).to.be.false; 247 | }); 248 | 249 | it('Should call the beforeSend hook if a hook has been registered.', () => { 250 | eventProcessorsStub.returns([]); 251 | hasHookStub.returns(true); 252 | 253 | Sentry.captureEvent(sentryEventStub); 254 | 255 | expect(hasHookStub.calledOnce).to.be.true; 256 | expect(callHookStub.calledOnce).to.be.true; 257 | }); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /test/unit/lib_sentry/scripts/services/sentryService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const proxyQuire = require('proxyquire').noCallThru(); 5 | const sinon = require('sinon'); 6 | 7 | require('app-module-path').addPath(process.cwd() + '/cartridges'); 8 | 9 | var createServiceStub = sinon.stub(); 10 | 11 | var sentryService = proxyQuire('lib_sentry/cartridge/scripts/services/sentryService', { 12 | '*/cartridge/config/sentry': { }, 13 | 'dw/svc/LocalServiceRegistry': { 14 | createService: createServiceStub 15 | }, 16 | 'dw/system/Logger': { 17 | getLogger: () => {} 18 | } 19 | }); 20 | 21 | describe('Sentry Service', () => { 22 | it('should create the service to be called.', () => { 23 | sentryService.sentryEvent(null, 'dns'); 24 | 25 | expect(createServiceStub.calledOnce).to.be.true; 26 | expect(createServiceStub.calledWith('Sentry')).to.be.true; 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var sgmfScripts = require('sgmf-scripts'); 5 | 6 | module.exports = [{ 7 | mode: 'production', 8 | name: 'js', 9 | entry: sgmfScripts.createJsPath(), 10 | output: { 11 | path: path.resolve('./cartridges/plugin_sentry/cartridge/static'), 12 | filename: '[name].js' 13 | } 14 | }]; 15 | --------------------------------------------------------------------------------