├── .cfconfig.json ├── .cfformat.json ├── .cflintrc ├── .editorconfig ├── .env.template ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.MD ├── FUNDING.YML ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── SUPPORT.md └── workflows │ ├── pr.yml │ ├── release.yml │ ├── snapshot.yml │ └── tests.yml ├── .gitignore ├── .markdownlint.json ├── .project ├── .settings └── org.eclipse.core.resources.prefs ├── ModuleConfig.cfc ├── box.json ├── build ├── Build.cfc └── release.boxr ├── changelog.md ├── models ├── SentryAppender.cfc └── SentryService.cfc ├── readme.md ├── server-adobe@2018.json ├── server-adobe@2021.json ├── server-adobe@2023.json ├── server-boxlang@1.json ├── server-lucee@5.json ├── server-lucee@6.json ├── settings.xml └── test-harness ├── .cflintrc ├── Application.cfc ├── box.json ├── config ├── Application.cfc ├── Coldbox.cfc ├── Routes.cfm └── WireBox.cfc ├── handlers └── Main.cfc ├── index.cfm ├── layouts └── Main.cfm └── tests ├── Application.cfc ├── index.cfm ├── runner.cfm └── specs └── SentryTests.cfc /.cfconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "debuggingEnabled":true, 3 | "debuggingReportExecutionTimes":false, 4 | "disableInternalCFJavaComponents":false, 5 | "inspectTemplate":"always", 6 | "requestTimeout":"0,0,0,90", 7 | "robustExceptionEnabled":true 8 | } -------------------------------------------------------------------------------- /.cfformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "array.empty_padding": false, 3 | "array.padding": true, 4 | "array.multiline.min_length": 50, 5 | "array.multiline.element_count": 2, 6 | "array.multiline.leading_comma.padding": true, 7 | "array.multiline.leading_comma": false, 8 | "alignment.consecutive.assignments": true, 9 | "alignment.consecutive.properties": true, 10 | "alignment.consecutive.params": true, 11 | "alignment.doc_comments" : true, 12 | "brackets.padding": true, 13 | "comment.asterisks": "align", 14 | "binary_operators.padding": true, 15 | "for_loop_semicolons.padding": true, 16 | "function_call.empty_padding": false, 17 | "function_call.padding": true, 18 | "function_call.multiline.leading_comma.padding": true, 19 | "function_call.casing.builtin": "cfdocs", 20 | "function_call.casing.userdefined": "camel", 21 | "function_call.multiline.element_count": 3, 22 | "function_call.multiline.leading_comma": false, 23 | "function_call.multiline.min_length": 50, 24 | "function_declaration.padding": true, 25 | "function_declaration.empty_padding": false, 26 | "function_declaration.multiline.leading_comma": false, 27 | "function_declaration.multiline.leading_comma.padding": true, 28 | "function_declaration.multiline.element_count": 3, 29 | "function_declaration.multiline.min_length": 50, 30 | "function_declaration.group_to_block_spacing": "compact", 31 | "function_anonymous.empty_padding": false, 32 | "function_anonymous.group_to_block_spacing": "compact", 33 | "function_anonymous.multiline.element_count": 3, 34 | "function_anonymous.multiline.leading_comma": false, 35 | "function_anonymous.multiline.leading_comma.padding": true, 36 | "function_anonymous.multiline.min_length": 50, 37 | "function_anonymous.padding": true, 38 | "indent_size": 4, 39 | "keywords.block_to_keyword_spacing": "spaced", 40 | "keywords.group_to_block_spacing": "spaced", 41 | "keywords.padding_inside_group": true, 42 | "keywords.spacing_to_block": "spaced", 43 | "keywords.spacing_to_group": true, 44 | "keywords.empty_group_spacing": false, 45 | "max_columns": 115, 46 | "metadata.multiline.element_count": 3, 47 | "metadata.multiline.min_length": 50, 48 | "method_call.chain.multiline" : 3, 49 | "newline":"\n", 50 | "property.multiline.element_count": 3, 51 | "property.multiline.min_length": 30, 52 | "parentheses.padding": true, 53 | "strings.quote": "double", 54 | "strings.attributes.quote": "double", 55 | "struct.separator": " : ", 56 | "struct.padding": true, 57 | "struct.empty_padding": false, 58 | "struct.multiline.leading_comma": false, 59 | "struct.multiline.leading_comma.padding": true, 60 | "struct.multiline.element_count": 2, 61 | "struct.multiline.min_length": 60, 62 | "tab_indent": true 63 | } 64 | -------------------------------------------------------------------------------- /.cflintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rule": [], 3 | "includes": [ 4 | { "code": "AVOID_USING_CFINCLUDE_TAG" }, 5 | { "code": "AVOID_USING_CFABORT_TAG" }, 6 | { "code": "AVOID_USING_CFEXECUTE_TAG" }, 7 | { "code": "AVOID_USING_DEBUG_ATTR" }, 8 | { "code": "AVOID_USING_ABORT" }, 9 | { "code": "AVOID_USING_ISDATE" }, 10 | { "code": "AVOID_USING_ISDEBUGMODE" }, 11 | { "code": "AVOID_USING_CFINSERT_TAG" }, 12 | { "code": "AVOID_USING_CFUPDATE_TAG" }, 13 | { "code": "ARG_VAR_CONFLICT" }, 14 | { "code": "ARG_HINT_MISSING" }, 15 | { "code": "ARG_HINT_MISSING_SCRIPT" }, 16 | { "code" : "ARGUMENT_INVALID_NAME" }, 17 | { "code" : "ARGUMENT_ALLCAPS_NAME" }, 18 | { "code" : "ARGUMENT_TOO_WORDY" }, 19 | { "code" : "ARGUMENT_IS_TEMPORARY" }, 20 | { "code": "CFQUERYPARAM_REQ" }, 21 | { "code": "COMPARE_INSTEAD_OF_ASSIGN" }, 22 | { "code": "COMPONENT_HINT_MISSING" }, 23 | { "code" : "COMPONENT_INVALID_NAME" }, 24 | { "code" : "COMPONENT_ALLCAPS_NAME" }, 25 | { "code" : "COMPONENT_TOO_SHORT" }, 26 | { "code" : "COMPONENT_TOO_LONG" }, 27 | { "code" : "COMPONENT_TOO_WORDY" }, 28 | { "code" : "COMPONENT_IS_TEMPORARY" }, 29 | { "code" : "COMPONENT_HAS_PREFIX_OR_POSTFIX" }, 30 | { "code": "COMPLEX_BOOLEAN_CHECK" }, 31 | { "code": "EXCESSIVE_FUNCTION_LENGTH" }, 32 | { "code": "EXCESSIVE_COMPONENT_LENGTH" }, 33 | { "code": "EXCESSIVE_ARGUMENTS" }, 34 | { "code": "EXCESSIVE_FUNCTIONS" }, 35 | { "code": "EXPLICIT_BOOLEAN_CHECK" }, 36 | { "code": "FUNCTION_TOO_COMPLEX" }, 37 | { "code": "FUNCTION_HINT_MISSING" }, 38 | { "code": "FILE_SHOULD_START_WITH_LOWERCASE" }, 39 | { "code": "LOCAL_LITERAL_VALUE_USED_TOO_OFTEN" }, 40 | { "code": "GLOBAL_LITERAL_VALUE_USED_TOO_OFTEN" }, 41 | { "code": "MISSING_VAR" }, 42 | { "code" : "METHOD_INVALID_NAME" }, 43 | { "code" : "METHOD_ALLCAPS_NAME" }, 44 | { "code" : "METHOD_IS_TEMPORARY" }, 45 | { "code": "NESTED_CFOUTPUT" }, 46 | { "code": "NEVER_USE_QUERY_IN_CFM" }, 47 | { "code": "OUTPUT_ATTR" }, 48 | { "code" : "QUERYPARAM_REQ" }, 49 | { "code": "UNUSED_LOCAL_VARIABLE" }, 50 | { "code": "UNUSED_METHOD_ARGUMENT" }, 51 | { "code": "SQL_SELECT_STAR" }, 52 | { "code": "SCOPE_ALLCAPS_NAME" }, 53 | { "code": "VAR_ALLCAPS_NAME" }, 54 | { "code": "VAR_INVALID_NAME" }, 55 | { "code": "VAR_TOO_WORDY" } 56 | ], 57 | "inheritParent": false, 58 | "parameters": { 59 | "TooManyFunctionsChecker.maximum" : 20 60 | } 61 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | indent_style = tab 11 | indent_size = 4 12 | tab_width = 4 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.{md,markdown}] 19 | trim_trailing_whitespace = false 20 | insert_final_newline = false -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | SENTRY_PUBLICKEY= 2 | SENTRY_PRIVATEKEY= 3 | SENTRY_PROJECTID= 4 | SENTRY_URL= -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.MD: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#code-of-conduct). 4 | -------------------------------------------------------------------------------- /.github/FUNDING.YML: -------------------------------------------------------------------------------- 1 | patreon: ortussolutions 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 7 | 8 | ## What are the steps to reproduce this issue? 9 | 10 | 1. … 11 | 2. … 12 | 3. … 13 | 14 | ## What happens? 15 | 16 | … 17 | 18 | ## What were you expecting to happen? 19 | 20 | … 21 | 22 | ## Any logs, error output, etc? 23 | 24 | … 25 | 26 | ## Any other comments? 27 | 28 | … 29 | 30 | ## What versions are you using? 31 | 32 | **Operating System:** … 33 | **Package Version:** … 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request a new feature or enhancement 4 | --- 5 | 6 | 7 | 8 | ## Summary 9 | 10 | 11 | 12 | ## Detailed Description 13 | 14 | 15 | 16 | ## Possible Implementation Ideas 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the changes and which issue(s) is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | **Please note that all PRs must have tests attached to them** 6 | 7 | IMPORTANT: Please review the [CONTRIBUTING.md](../CONTRIBUTING.md) file for detailed contributing guidelines. 8 | 9 | ## Issues 10 | 11 | All PRs must have an accompanied issue. Please make sure you created it and linked it here. 12 | 13 | ## Type of change 14 | 15 | Please delete options that are not relevant. 16 | 17 | - [ ] Bug Fix 18 | - [ ] Improvement 19 | - [ ] New Feature 20 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 21 | - [ ] This change requires a documentation update 22 | 23 | ## Checklist 24 | 25 | - [ ] My code follows the style guidelines of this project [cfformat](../.cfformat.json) 26 | - [ ] I have commented my code, particularly in hard-to-understand areas 27 | - [ ] I have made corresponding changes to the documentation 28 | - [ ] I have added tests that prove my fix is effective or that my feature works 29 | - [ ] New and existing unit tests pass locally with my changes 30 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#security-vulnerabilities). 4 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support & Help 2 | 3 | Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#support-questions). 4 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "main" 7 | - "master" 8 | - "development" 9 | - "releases/v*" 10 | pull_request: 11 | branches: 12 | - "releases/v*" 13 | - development 14 | 15 | jobs: 16 | tests: 17 | uses: ./.github/workflows/tests.yml 18 | secrets: inherit 19 | 20 | # Format PR 21 | format_check: 22 | name: Checks Source Code Formatting 23 | runs-on: ubuntu-24.04 24 | steps: 25 | - name: Checkout Repository 26 | uses: actions/checkout@v4 27 | 28 | - uses: Ortus-Solutions/commandbox-action@v1.0.2 29 | with: 30 | cmd: run-script format:check 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build a Release 2 | 3 | on: 4 | # If you push to master|main this will trigger a stable release 5 | push: 6 | branches: 7 | - master 8 | - main 9 | 10 | # Reusable workflow : Usually called by a `snapshot` workflow 11 | workflow_call: 12 | inputs: 13 | snapshot: 14 | description: 'Is this a snapshot build?' 15 | required: false 16 | default: false 17 | type: boolean 18 | 19 | env: 20 | MODULE_ID: sentry 21 | SNAPSHOT: ${{ inputs.snapshot || false }} 22 | 23 | jobs: 24 | ########################################################################################## 25 | # Build & Publish 26 | ########################################################################################## 27 | build: 28 | name: Build & Publish 29 | runs-on: ubuntu-24.04 30 | steps: 31 | - name: Checkout Repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Setup CommandBox 35 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 36 | with: 37 | forgeboxAPIKey: ${{ secrets.FORGEBOX_TOKEN }} 38 | 39 | - name: "Setup Environment Variables For Build Process" 40 | id: current_version 41 | run: | 42 | echo "VERSION=`cat box.json | jq '.version' -r`" >> $GITHUB_ENV 43 | box package set version=@build.version@+@build.number@ 44 | # master or snapshot 45 | echo "Github Ref is $GITHUB_REF" 46 | echo "BRANCH=master" >> $GITHUB_ENV 47 | if [ $GITHUB_REF == 'refs/heads/development' ] 48 | then 49 | echo "BRANCH=development" >> $GITHUB_ENV 50 | fi 51 | 52 | - name: Update changelog [unreleased] with latest version 53 | uses: thomaseizinger/keep-a-changelog-new-release@1.3.0 54 | if: env.SNAPSHOT == 'false' 55 | with: 56 | changelogPath: ./changelog.md 57 | tag: v${{ env.VERSION }} 58 | 59 | - name: Build ${{ env.MODULE_ID }} 60 | run: | 61 | npm install -g markdownlint-cli 62 | markdownlint changelog.md --fix 63 | box install commandbox-docbox 64 | box task run taskfile=build/Build target=run :version=${{ env.VERSION }} :projectName=${{ env.MODULE_ID }} :buildID=${{ github.run_number }} :branch=${{ env.BRANCH }} 65 | 66 | - name: Commit Changelog To Master 67 | uses: EndBug/add-and-commit@v9.1.4 68 | if: env.SNAPSHOT == 'false' 69 | with: 70 | author_name: Github Actions 71 | author_email: info@ortussolutions.com 72 | message: 'Finalized changelog for v${{ env.VERSION }}' 73 | add: changelog.md 74 | 75 | - name: Tag Version 76 | uses: rickstaa/action-create-tag@v1.7.2 77 | if: env.SNAPSHOT == 'false' 78 | with: 79 | tag: "v${{ env.VERSION }}" 80 | force_push_tag: true 81 | message: "Latest Release v${{ env.VERSION }}" 82 | 83 | - name: Upload Build Artifacts 84 | if: success() 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: ${{ env.MODULE_ID }} 88 | path: | 89 | .artifacts/**/* 90 | changelog.md 91 | 92 | - name: Upload Binaries to S3 93 | uses: jakejarvis/s3-sync-action@master 94 | with: 95 | args: --acl public-read 96 | env: 97 | AWS_S3_BUCKET: "downloads.ortussolutions.com" 98 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }} 99 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_SECRET }} 100 | SOURCE_DIR: ".artifacts/${{ env.MODULE_ID }}" 101 | DEST_DIR: "ortussolutions/coldbox-modules/${{ env.MODULE_ID }}" 102 | 103 | - name: Upload API Docs to S3 104 | uses: jakejarvis/s3-sync-action@master 105 | with: 106 | args: --acl public-read 107 | env: 108 | AWS_S3_BUCKET: "apidocs.ortussolutions.com" 109 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }} 110 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_SECRET }} 111 | SOURCE_DIR: ".tmp/apidocs" 112 | DEST_DIR: "coldbox-modules/${{ env.MODULE_ID }}/${{ env.VERSION }}" 113 | 114 | - name: Publish To ForgeBox 115 | run: | 116 | cd .tmp/${{ env.MODULE_ID }} 117 | cat box.json 118 | box forgebox publish --force 119 | 120 | - name: Create Github Release 121 | uses: taiki-e/create-gh-release-action@v1.8.0 122 | continue-on-error: true 123 | if: env.SNAPSHOT == 'false' 124 | with: 125 | title: ${{ env.VERSION }} 126 | changelog: changelog.md 127 | token: ${{ secrets.GITHUB_TOKEN }} 128 | ref: refs/tags/v${{ env.VERSION }} 129 | 130 | ########################################################################################## 131 | # Prep Next Release 132 | ########################################################################################## 133 | prep_next_release: 134 | name: Prep Next Release 135 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' 136 | runs-on: ubuntu-24.04 137 | needs: [ build ] 138 | steps: 139 | # Checkout development 140 | - name: Checkout Repository 141 | uses: actions/checkout@v4 142 | with: 143 | ref: development 144 | 145 | - name: Setup CommandBox 146 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 147 | with: 148 | forgeboxAPIKey: ${{ secrets.FORGEBOX_TOKEN }} 149 | 150 | - name: Download build artifacts 151 | uses: actions/download-artifact@v4 152 | with: 153 | name: ${{ env.MODULE_ID }} 154 | path: .tmp 155 | 156 | # Copy the changelog to the development branch 157 | - name: Copy Changelog 158 | run: | 159 | cp .tmp/changelog.md changelog.md 160 | 161 | # Bump to next version 162 | - name: Bump Version 163 | run: | 164 | box bump --patch --!TagVersion 165 | 166 | # Commit it back to development 167 | - name: Commit Version Bump 168 | uses: EndBug/add-and-commit@v9.1.4 169 | with: 170 | author_name: Github Actions 171 | author_email: info@ortussolutions.com 172 | message: 'Version bump' 173 | add: | 174 | box.json 175 | changelog.md 176 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Build Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'development' 7 | workflow_dispatch: 8 | 9 | # Unique group name per workflow-branch/tag combo 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | ########################################################################################## 16 | # Module Tests 17 | ########################################################################################## 18 | tests: 19 | secrets: inherit 20 | uses: ./.github/workflows/tests.yml 21 | 22 | ########################################################################################## 23 | # Format Source Code 24 | ########################################################################################## 25 | format: 26 | name: Code Auto-Formatting 27 | runs-on: ubuntu-24.04 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Auto-format 32 | uses: Ortus-Solutions/commandbox-action@v1.0.2 33 | with: 34 | cmd: run-script format 35 | 36 | - name: Commit Format Changes 37 | uses: stefanzweifel/git-auto-commit-action@v5 38 | with: 39 | commit_message: Apply cfformat changes 40 | 41 | ########################################################################################## 42 | # Release it 43 | ########################################################################################## 44 | release: 45 | uses: ./.github/workflows/release.yml 46 | needs: [ tests, format ] 47 | secrets: inherit 48 | with: 49 | snapshot: true 50 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test Suites 2 | 3 | # We are a reusable Workflow only 4 | on: 5 | workflow_call: 6 | secrets: 7 | SLACK_WEBHOOK_URL: 8 | required: false 9 | 10 | jobs: 11 | tests: 12 | name: Tests 13 | runs-on: ubuntu-24.04 14 | env: 15 | DB_USER: root 16 | DB_PASSWORD: root 17 | continue-on-error: ${{ matrix.experimental }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | cfengine: [ "lucee@5", "adobe@2018", "adobe@2021", "adobe@2023" ] 22 | coldboxVersion: [ "^6.0.0", "^7.0.0" ] 23 | experimental: [ false ] 24 | # Here we tests all engines against ColdBox@BE 25 | include: 26 | - coldboxVersion: "be" 27 | cfengine: "lucee@5" 28 | experimental: true 29 | - coldboxVersion: "be" 30 | cfengine: "lucee@6" 31 | experimental: true 32 | - coldboxVersion: "be" 33 | cfengine: "adobe@2021" 34 | experimental: true 35 | - coldboxVersion: "be" 36 | cfengine: "adobe@2023" 37 | experimental: true 38 | - coldboxVersion: "be" 39 | cfengine: "boxlang@1" 40 | experimental: true 41 | steps: 42 | - name: Checkout Repository 43 | uses: actions/checkout@v4 44 | 45 | # - name: Setup Database and Fixtures 46 | # run: | 47 | # sudo systemctl start mysql.service 48 | # mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} -e 'CREATE DATABASE mementifier;' 49 | # mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} < test-harness/tests/resources/coolblog.sql 50 | 51 | - name: Setup Java 52 | uses: actions/setup-java@v4 53 | with: 54 | distribution: "temurin" 55 | java-version: "11" 56 | 57 | - name: Setup CommandBox CLI 58 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 59 | 60 | - name: Update Commandbox Boxlang Module 61 | if: ${{ matrix.cfengine == 'boxlang@1' }} 62 | run: 63 | box install --force commandbox-boxlang 64 | 65 | # Not Needed in this module 66 | #- name: Setup Environment For Testing Process 67 | # run: | 68 | # # Setup .env 69 | # touch .env 70 | # # ENV 71 | # printf "DB_HOST=localhost\n" >> .env 72 | # printf "DB_DATABASE=mydatabase\n" >> .env 73 | # printf "DB_DRIVER=MySQL\n" >> .env 74 | # printf "DB_USER=${{ env.DB_USER }}\n" >> .env 75 | # printf "DB_PASSWORD=${{ env.DB_PASSWORD }}\n" >> .env 76 | # printf "DB_CLASS=com.mysql.cj.jdbc.Driver\n" >> .env 77 | # printf "DB_BUNDLEVERSION=8.0.19\n" >> .env 78 | # printf "DB_BUNDLENAME=com.mysql.cj\n" >> .env 79 | 80 | - name: Install Test Harness with ColdBox ${{ matrix.coldboxVersion }} 81 | run: | 82 | box install 83 | cd test-harness 84 | box package set dependencies.coldbox=${{ matrix.coldboxVersion }} 85 | box install 86 | 87 | - name: Start ${{ matrix.cfengine }} Server 88 | run: | 89 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 90 | curl http://127.0.0.1:60299 91 | 92 | - name: Run Tests 93 | run: | 94 | mkdir -p test-harness/tests/results 95 | box testbox run --verbose outputFile=test-harness/tests/results/test-results outputFormats=json,antjunit 96 | 97 | - name: Publish Test Results 98 | uses: EnricoMi/publish-unit-test-result-action@v2 99 | if: always() 100 | with: 101 | junit_files: test-harness/tests/results/**/*.xml 102 | check_name: "${{ matrix.cfengine }} ColdBox ${{ matrix.coldboxVersion }} Test Results" 103 | 104 | - name: Upload Test Results to Artifacts 105 | if: always() 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: test-results-${{ matrix.cfengine }}-${{ matrix.coldboxVersion }} 109 | path: | 110 | test-harness/tests/results/**/* 111 | 112 | - name: Show Server Log On Failures 113 | if: ${{ failure() }} 114 | run: | 115 | box server log serverConfigFile="server-${{ matrix.cfengine }}.json" 116 | 117 | - name: Upload Debug Logs To Artifacts 118 | if: ${{ failure() }} 119 | uses: actions/upload-artifact@v4 120 | with: 121 | name: Failure Debugging Info - ${{ matrix.cfengine }} - ${{ matrix.coldboxVersion }} 122 | path: | 123 | .engine/**/logs/* 124 | .engine/**/WEB-INF/cfusion/logs/* 125 | 126 | - name: Slack Notifications 127 | # Only on failures and NOT in pull requests 128 | if: ${{ failure() && !startsWith( 'pull_request', github.event_name ) }} 129 | uses: rtCamp/action-slack-notify@v2 130 | env: 131 | SLACK_CHANNEL: coding 132 | SLACK_COLOR: ${{ job.status }} # or a specific color like 'green' or '#ff00ff' 133 | SLACK_ICON_EMOJI: ":bell:" 134 | SLACK_MESSAGE: '${{ github.repository }} tests failed :cry:' 135 | SLACK_TITLE: ${{ github.repository }} Tests For ${{ matrix.cfengine }} with ColdBox ${{ matrix.coldboxVersion }} failed 136 | SLACK_USERNAME: CI 137 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | .artifacts/** 4 | .tmp/** 5 | .engine/** 6 | 7 | test-harness/.engine/** 8 | test-harness/coldbox/** 9 | test-harness/docbox/** 10 | test-harness/testbox/** 11 | test-harness/logs/** 12 | test-harness/modules/** 13 | 14 | # log files 15 | logs/** 16 | 17 | # don't put token 18 | test-harness/config/token.cfm 19 | test-harness/.env 20 | /modules/ -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "single-h1": false, 4 | "no-hard-tabs" : false, 5 | "fenced-code-language" : false, 6 | "no-bare-urls" : false, 7 | "first-line-h1": false, 8 | "no-multiple-blanks": { 9 | "maximum": 2 10 | }, 11 | "no-duplicate-header" : { 12 | "siblings_only" : true 13 | }, 14 | "no-duplicate-heading" : false, 15 | "no-inline-html" : false 16 | } 17 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | sentry 4 | 5 | 6 | 7 | 8 | 9 | 10 | com.adobe.ide.coldfusion.projectNature 11 | 12 | 13 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding/=UTF-8 3 | -------------------------------------------------------------------------------- /ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | ********************************************************************************* 3 | * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | * www.ortussolutions.com 5 | * --- 6 | * Module Config. 7 | */ 8 | component { 9 | 10 | // Module Properties 11 | this.title = "sentry"; 12 | this.author = "Ortus Solutions"; 13 | this.webURL = "https://www.ortussolutions.com"; 14 | this.description = "A module to log and send bug reports to Sentry"; 15 | this.version = "@build.version@+@build.number@"; 16 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 17 | this.viewParentLookup = true; 18 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 19 | this.layoutParentLookup = true; 20 | this.cfmapping = "sentry"; 21 | this.dependencies = [ "funclinenums" ]; 22 | 23 | // STATIC SCRUB FIELDS 24 | variables.SCRUB_FIELDS = [ 25 | "passwd", 26 | "password", 27 | "password_confirmation", 28 | "secret", 29 | "confirm_password", 30 | "secret_token", 31 | "APIToken", 32 | "x-api-token", 33 | "fwreinit" 34 | ]; 35 | variables.SCRUB_HEADERS = [ "x-api-token", "Authorization" ]; 36 | 37 | /** 38 | * Configure 39 | */ 40 | function configure(){ 41 | settings = { 42 | // Sentry token 43 | "ServerSideToken" : "", 44 | // Enable the Sentry LogBox Appender Bridge 45 | "enableLogBoxAppender" : true, 46 | "async" : true, 47 | // Min/Max levels for appender 48 | "levelMin" : "FATAL", 49 | "levelMax" : "ERROR", 50 | // Enable/disable error logging 51 | "enableExceptionLogging" : true, 52 | // Sentry recommends not sending cookie and form data by default 53 | "sendCookies" : false, 54 | "sendPostData" : false, 55 | // Data sanitization, scrub fields and headers, replaced with "[Filtered]" at runtime 56 | "scrubFields" : [], 57 | "scrubHeaders" : [], 58 | "release" : "", 59 | "environment" : ( !isNull( controller ) ? controller.getSetting( "environment" ) : "" ), 60 | "DSN" : "", 61 | "publicKey" : "", 62 | "privateKey" : "", 63 | "projectID" : 0, 64 | "sentryUrl" : "https://sentry.io", 65 | // posting to "#sentryUrl#/api/#projectID#/store" is deprecated, but backward compatible 66 | // set to "envelope" to send events to modern "#sentryUrl#/api/#projectID#/envelope" 67 | "sentryEventEndpoint" : "store", 68 | "serverName" : cgi.server_name, 69 | "appRoot" : expandPath( "/" ), 70 | "sentryVersion" : 7, 71 | // This is not arbitrary but must be a specific value. Leave as "cfml" 72 | // https://docs.sentry.io/development/sdk-dev/attributes/ 73 | "platform" : "cfml", 74 | "logger" : ( !isNull( controller ) ? controller.getSetting( "appName" ) : "sentry" ), 75 | "userInfoUDF" : "", 76 | "extraInfoUDFs" : {} 77 | }; 78 | 79 | // Try to look up the release based on a box.json 80 | if ( !isNull( appmapping ) ) { 81 | var boxJSONPath = expandPath( "/" & appmapping & "/box.json" ); 82 | if ( fileExists( boxJSONPath ) ) { 83 | var boxJSONRaw = fileRead( boxJSONPath ); 84 | if ( isJSON( boxJSONRaw ) ) { 85 | var boxJSON = deserializeJSON( boxJSONRaw ); 86 | if ( boxJSON.keyExists( "version" ) ) { 87 | settings.release = boxJSON.version; 88 | if ( boxJSON.keyExists( "slug" ) ) { 89 | settings.release = boxJSON.slug & "@" & settings.release; 90 | } else if ( boxJSON.keyExists( "name" ) ) { 91 | settings.release = boxJSON.name & "@" & settings.release; 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | interceptorSettings = { customInterceptionPoints : [ "onSentryEventCapture" ] }; 99 | } 100 | 101 | /** 102 | * Fired when the module is registered and activated. 103 | */ 104 | function onLoad(){ 105 | // Incorporate defaults into settings 106 | settings.scrubFields.addAll( SCRUB_FIELDS ); 107 | settings.scrubHeaders.addAll( SCRUB_HEADERS ); 108 | 109 | // Load the LogBox Appenders 110 | if ( settings.enableLogBoxAppender ) { 111 | loadAppenders(); 112 | } 113 | } 114 | 115 | /** 116 | * Fired when the module is unregistered and unloaded 117 | */ 118 | function onUnload(){ 119 | } 120 | 121 | /** 122 | * Trap exceptions and send them to Sentry 123 | */ 124 | function onException( event, interceptData, buffer ){ 125 | if ( !settings.enableExceptionLogging ) { 126 | return; 127 | } 128 | if ( wirebox.containsInstance( "SentryService@sentry" ) ) { 129 | var sentryService = wirebox.getInstance( "SentryService@sentry" ); 130 | 131 | sentryService.captureException( exception = interceptData.exception, level = "error" ); 132 | } 133 | } 134 | 135 | // **************************************** PRIVATE ************************************************// 136 | 137 | /** 138 | * Load LogBox Appenders 139 | */ 140 | private function loadAppenders(){ 141 | // Get config 142 | /*var logBoxConfig = logBox.getConfig(); 143 | var rootConfig = ''; 144 | 145 | // Register tracer appender 146 | rootConfig = logBoxConfig.getRoot(); 147 | logBoxConfig.appender( 148 | name = 'sentry_appender', 149 | class = '#moduleMapping#.models.SentryAppender', 150 | levelMin = settings.levelMin, 151 | levelMax = settings.levelMax 152 | ); 153 | logBoxConfig.root( 154 | levelMin = rootConfig.levelMin, 155 | levelMax = rootConfig.levelMax, 156 | appenders= listAppend( rootConfig.appenders, 'sentry_appender') 157 | ); 158 | 159 | // Store back config 160 | logBox.configure( logBoxConfig );*/ 161 | 162 | logBox.registerAppender( 163 | name = "sentry_appender", 164 | class = "#moduleMapping#.models.SentryAppender", 165 | levelMin = logBox.logLevels[ settings.levelMin ], 166 | levelMax = logBox.logLevels[ settings.levelMax ] 167 | ); 168 | 169 | var appenders = logBox.getAppendersMap( "sentry_appender" ); 170 | // Register the appender with the root loggger, and turn the logger on. 171 | var root = logBox.getRootLogger(); 172 | root.addAppender( appenders[ "sentry_appender" ] ); 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Sentry", 3 | "author":"Ortus Solutions ", 4 | "version":"2.1.6", 5 | "slug":"sentry", 6 | "type":"modules", 7 | "homepage":"https://github.com/coldbox-modules/sentry", 8 | "documentation":"https://github.com/coldbox-modules/sentry", 9 | "repository":{ 10 | "type":"git", 11 | "url":"https://github.com/coldbox-modules/sentry" 12 | }, 13 | "bugs":"https://github.com/coldbox-modules/sentry/issues", 14 | "shortDescription":"A module to log messages and send bug reports to Sentry", 15 | "license":[ 16 | { 17 | "type":"Apache2", 18 | "url":"http://www.apache.org/licenses/LICENSE-2.0.html" 19 | } 20 | ], 21 | "contributors":[], 22 | "dependencies":{ 23 | "funclinenums":"^1.1.0" 24 | }, 25 | "devDependencies":{}, 26 | "installPaths":{ 27 | "funclinenums":"modules/funclinenums/" 28 | }, 29 | "ignore":[ 30 | "**/.*", 31 | "test-harness", 32 | "*.md" 33 | ], 34 | "scripts":{ 35 | "build:module":"task run taskFile=build/Build.cfc :projectName=`package show slug` :version=`package show version`", 36 | "build:docs":"task run taskFile=build/Build.cfc target=docs :projectName=`package show slug` :version=`package show version`", 37 | "install:dependencies":"install --force && cd test-harness && install --force", 38 | "release":"recipe build/release.boxr", 39 | "format":"cfformat run helpers,models,interceptors,handlers,test-harness/tests/,ModuleConfig.cfc --overwrite", 40 | "format:watch":"cfformat watch helpers,models,interceptors,handlers,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json", 41 | "format:check":"cfformat check helpers,models,interceptors,handlers,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json", 42 | "start:lucee":"server start serverConfigFile=server-lucee@5.json", 43 | "start:2018":"server start serverConfigFile=server-adobe@2018.json", 44 | "start:2021":"server start serverConfigFile=server-adobe@2021.json", 45 | "stop:lucee":"server stop serverConfigFile=server-lucee@5.json", 46 | "stop:2018":"server stop serverConfigFile=server-adobe@2018.json", 47 | "stop:2021":"server stop serverConfigFile=server-adobe@2021.json", 48 | "logs:lucee":"server log serverConfigFile=server-lucee@5.json --follow", 49 | "logs:2018":"server log serverConfigFile=server-adobe@2018.json --follow", 50 | "logs:2021":"server log serverConfigFile=server-adobe@2021.json --follow" 51 | }, 52 | "testbox":{ 53 | "runner":"http://localhost:60299/tests/runner.cfm" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /build/Build.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Build process for ColdBox Modules 3 | * Adapt to your needs. 4 | */ 5 | component { 6 | 7 | /** 8 | * Constructor 9 | */ 10 | function init(){ 11 | // Setup Pathing 12 | variables.cwd = getCWD().reReplace( "\.$", "" ); 13 | variables.artifactsDir = cwd & "/.artifacts"; 14 | variables.buildDir = cwd & "/.tmp"; 15 | variables.apidDocsDir = variables.buildDir & "/apidocs"; 16 | variables.apiDocsURL = "http://localhost:60299/apidocs/"; 17 | variables.testRunner = "http://localhost:60299/tests/runner.cfm"; 18 | 19 | // Source Excludes Not Added to final binary 20 | variables.excludes = [ 21 | "build", 22 | "node-modules", 23 | "resources", 24 | "test-harness", 25 | "(package|package-lock).json", 26 | "webpack.config.js", 27 | "server-.*\.json", 28 | "docker-compose.yml", 29 | "^\..*" 30 | ]; 31 | 32 | // Cleanup + Init Build Directories 33 | [ 34 | variables.buildDir, 35 | variables.artifactsDir, 36 | variables.apidDocsDir 37 | ].each( function( item ){ 38 | if ( directoryExists( item ) ) { 39 | directoryDelete( item, true ); 40 | } 41 | // Create directories 42 | directoryCreate( item, true, true ); 43 | } ); 44 | 45 | // Create Mappings 46 | fileSystemUtil.createMapping( 47 | "coldbox", 48 | variables.cwd & "test-harness/coldbox" 49 | ); 50 | 51 | return this; 52 | } 53 | 54 | /** 55 | * Run the build process: test, build source, docs, checksums 56 | * 57 | * @projectName The project name used for resources and slugs 58 | * @version The version you are building 59 | * @buldID The build identifier 60 | * @branch The branch you are building 61 | */ 62 | function run( 63 | required projectName, 64 | version = "1.0.0", 65 | buildID = createUUID(), 66 | branch = "development" 67 | ){ 68 | // Create project mapping 69 | fileSystemUtil.createMapping( arguments.projectName, variables.cwd ); 70 | 71 | // Build the source 72 | buildSource( argumentCollection = arguments ); 73 | 74 | // Build Docs 75 | arguments.outputDir = variables.buildDir & "/apidocs"; 76 | docs( argumentCollection = arguments ); 77 | 78 | // checksums 79 | buildChecksums(); 80 | 81 | // Finalize Message 82 | print 83 | .line() 84 | .boldMagentaLine( "Build Process is done! Enjoy your build!" ) 85 | .toConsole(); 86 | } 87 | 88 | /** 89 | * Run the test suites 90 | */ 91 | function runTests(){ 92 | // Tests First, if they fail then exit 93 | print.blueLine( "Testing the package, please wait..." ).toConsole(); 94 | 95 | command( "testbox run" ) 96 | .params( 97 | runner = variables.testRunner, 98 | verbose = true, 99 | outputFile = "#variables.cwd#/test-harness/results/test-results", 100 | outputFormats="json,antjunit" 101 | ) 102 | .run(); 103 | 104 | // Check Exit Code? 105 | if ( shell.getExitCode() ) { 106 | return error( "Cannot continue building, tests failed!" ); 107 | } 108 | } 109 | 110 | /** 111 | * Build the source 112 | * 113 | * @projectName The project name used for resources and slugs 114 | * @version The version you are building 115 | * @buldID The build identifier 116 | * @branch The branch you are building 117 | */ 118 | function buildSource( 119 | required projectName, 120 | version = "1.0.0", 121 | buildID = createUUID(), 122 | branch = "development" 123 | ){ 124 | // Build Notice ID 125 | print 126 | .line() 127 | .boldMagentaLine( 128 | "Building #arguments.projectName# v#arguments.version#+#arguments.buildID# from #cwd# using the #arguments.branch# branch." 129 | ) 130 | .toConsole(); 131 | 132 | ensureExportDir( argumentCollection = arguments ); 133 | 134 | // Project Build Dir 135 | variables.projectBuildDir = variables.buildDir & "/#projectName#"; 136 | directoryCreate( 137 | variables.projectBuildDir, 138 | true, 139 | true 140 | ); 141 | 142 | // Copy source 143 | print.blueLine( "Copying source to build folder..." ).toConsole(); 144 | copy( 145 | variables.cwd, 146 | variables.projectBuildDir 147 | ); 148 | 149 | // Create build ID 150 | fileWrite( 151 | "#variables.projectBuildDir#/#projectName#-#version#+#buildID#", 152 | "Built with love on #dateTimeFormat( now(), "full" )#" 153 | ); 154 | 155 | // Updating Placeholders 156 | print.greenLine( "Updating version identifier to #arguments.version#" ).toConsole(); 157 | command( "tokenReplace" ) 158 | .params( 159 | path = "/#variables.projectBuildDir#/**", 160 | token = "@build.version@", 161 | replacement = arguments.version 162 | ) 163 | .run(); 164 | 165 | print.greenLine( "Updating build identifier to #arguments.buildID#" ).toConsole(); 166 | command( "tokenReplace" ) 167 | .params( 168 | path = "/#variables.projectBuildDir#/**", 169 | token = ( arguments.branch == "master" ? "@build.number@" : "+@build.number@" ), 170 | replacement = ( arguments.branch == "master" ? arguments.buildID : "-snapshot" ) 171 | ) 172 | .run(); 173 | 174 | // zip up source 175 | var destination = "#variables.exportsDir#/#projectName#-#version#.zip"; 176 | print.greenLine( "Zipping code to #destination#" ).toConsole(); 177 | cfzip( 178 | action = "zip", 179 | file = "#destination#", 180 | source = "#variables.projectBuildDir#", 181 | overwrite = true, 182 | recurse = true 183 | ); 184 | 185 | // Copy box.json for convenience 186 | fileCopy( 187 | "#variables.projectBuildDir#/box.json", 188 | variables.exportsDir 189 | ); 190 | } 191 | 192 | /** 193 | * Produce the API Docs 194 | */ 195 | function docs( 196 | required projectName, 197 | version = "1.0.0", 198 | outputDir = ".tmp/apidocs" 199 | ){ 200 | ensureExportDir( argumentCollection = arguments ); 201 | 202 | // Create project mapping 203 | fileSystemUtil.createMapping( arguments.projectName, variables.cwd ); 204 | // Generate Docs 205 | print.greenLine( "Generating API Docs, please wait..." ).toConsole(); 206 | 207 | command( "docbox generate" ) 208 | .params( 209 | "source" = "models", 210 | "mapping" = "models", 211 | "strategy-projectTitle" = "#arguments.projectName# v#arguments.version#", 212 | "strategy-outputDir" = arguments.outputDir 213 | ) 214 | .run(); 215 | 216 | print.greenLine( "API Docs produced at #arguments.outputDir#" ).toConsole(); 217 | 218 | var destination = "#variables.exportsDir#/#projectName#-docs-#version#.zip"; 219 | print.greenLine( "Zipping apidocs to #destination#" ).toConsole(); 220 | cfzip( 221 | action = "zip", 222 | file = "#destination#", 223 | source = "#arguments.outputDir#", 224 | overwrite = true, 225 | recurse = true 226 | ); 227 | } 228 | 229 | /********************************************* PRIVATE HELPERS *********************************************/ 230 | 231 | /** 232 | * Build Checksums 233 | */ 234 | private function buildChecksums(){ 235 | print.greenLine( "Building checksums" ).toConsole(); 236 | command( "checksum" ) 237 | .params( 238 | path = "#variables.exportsDir#/*.zip", 239 | algorithm = "SHA-512", 240 | extension = "sha512", 241 | write = true 242 | ) 243 | .run(); 244 | command( "checksum" ) 245 | .params( 246 | path = "#variables.exportsDir#/*.zip", 247 | algorithm = "md5", 248 | extension = "md5", 249 | write = true 250 | ) 251 | .run(); 252 | } 253 | 254 | /** 255 | * DirectoryCopy is broken in lucee 256 | */ 257 | private function copy( src, target, recurse = true ){ 258 | // process paths with excludes 259 | directoryList( 260 | src, 261 | false, 262 | "path", 263 | function( path ){ 264 | var isExcluded = false; 265 | variables.excludes.each( function( item ){ 266 | if ( path.replaceNoCase( variables.cwd, "", "all" ).reFindNoCase( item ) ) { 267 | isExcluded = true; 268 | } 269 | } ); 270 | return !isExcluded; 271 | } 272 | ).each( function( item ){ 273 | // Copy to target 274 | if ( fileExists( item ) ) { 275 | print.blueLine( "Copying #item#" ).toConsole(); 276 | fileCopy( item, target ); 277 | } else { 278 | print.greenLine( "Copying directory #item#" ).toConsole(); 279 | directoryCopy( 280 | item, 281 | target & "/" & item.replace( src, "" ), 282 | true 283 | ); 284 | } 285 | } ); 286 | } 287 | 288 | /** 289 | * Gets the last Exit code to be used 290 | **/ 291 | private function getExitCode(){ 292 | return ( createObject( "java", "java.lang.System" ).getProperty( "cfml.cli.exitCode" ) ?: 0 ); 293 | } 294 | 295 | /** 296 | * Ensure the export directory exists at artifacts/NAME/VERSION/ 297 | */ 298 | private function ensureExportDir( 299 | required projectName, 300 | version = "1.0.0" 301 | ){ 302 | if ( structKeyExists( variables, "exportsDir" ) && directoryExists( variables.exportsDir ) ){ 303 | return; 304 | } 305 | // Prepare exports directory 306 | variables.exportsDir = variables.artifactsDir & "/#projectName#/#arguments.version#"; 307 | directoryCreate( variables.exportsDir, true, true ); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /build/release.boxr: -------------------------------------------------------------------------------- 1 | # This recipe signifies a new release of the module by doing merges and bumps accordingly 2 | 3 | # Check out master and update it locally 4 | !git checkout -f master 5 | !git pull origin master 6 | 7 | # Merge development into it for release 8 | !git merge --no-ff development 9 | 10 | # Push all branches back out to github 11 | !git push origin --all 12 | 13 | # Check development again 14 | !git checkout -f development 15 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | * * * 9 | 10 | ## [Unreleased] 11 | 12 | ## [2.1.5] - 2024-12-25 13 | 14 | ### Fixed - [Issue #37](https://github.com/coldbox-modules/sentry/issues/37) - Fix var-scoping inside conditional 15 | 16 | ## [2.1.4] - 2024-12-24 17 | 18 | ### Fixed - [Issue #37](https://github.com/coldbox-modules/sentry/issues/37) - Resolves an ACF incompat issue where traceparent variable was not available in threaded logging 19 | 20 | ## [2.1.3] - 2024-11-14 21 | 22 | ### Fixed 23 | 24 | - Fixes for Coldbox being detected incorrectly. 25 | 26 | ## [2.1.0] - 2024-11-12 27 | 28 | - Fixes for Coldbox being detected incorrectly. 29 | 30 | ## [2.1.0] - 2024-11-12 31 | 32 | ### Added 33 | 34 | - Added support for passing through [`traceparent` headers](https://www.w3.org/TR/trace-context/#traceparent-header) and, optionally, traceparent data provided by [the `cbotel` module](https://forgebox.io/view/cbotel) 35 | - Added `onSentryEventCapture` interception in Coldbox context to allow contributions to tags and user info 36 | 37 | ## [2.0.0] - 2024-06-10 38 | 39 | ### Changed 40 | 41 | - Update the event structure to the new format Sentry has adopted for their official SDKs 42 | - Don't send cookie and form scope data by default 43 | 44 | ### Added 45 | 46 | - Add support for the new `/api/{project_id}/envelope` endpoint Sentry has adopted for sending events 47 | 48 | ## [1.0.0] - 2019-05-10 49 | 50 | ### Added 51 | 52 | - Create first module version 53 | 54 | [Unreleased]: https://github.com/coldbox-modules/sentry/compare/v2.1.5...HEAD 55 | 56 | [2.1.5]: https://github.com/coldbox-modules/sentry/compare/v2.1.4...v2.1.5 57 | 58 | [2.1.4]: https://github.com/coldbox-modules/sentry/compare/HEAD...v2.1.4 59 | 60 | [2.1.3]: https://github.com/coldbox-modules/sentry/compare/v2.1.0...v2.1.3 61 | 62 | [2.1.0]: https://github.com/coldbox-modules/sentry/compare/57864cae5969ad38eee194db5a6b2798e91967b3...v2.1.0 63 | -------------------------------------------------------------------------------- /models/SentryAppender.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | ********************************************************************************* 3 | * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | * www.ortussolutions.com 5 | * --- 6 | * Appender for Sentry leveraging the Sentry service 7 | */ 8 | component extends="coldbox.system.logging.AbstractAppender" accessors=true { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | function init( 14 | required name, 15 | struct properties = structNew(), 16 | layout = "", 17 | numeric levelMin = 0, 18 | numeric levelMax = 4 19 | ){ 20 | super.init( argumentCollection = arguments ); 21 | 22 | // Get sentry Service from WireBox if it's not already passed to the appender 23 | if ( !propertyExists( "sentryService" ) ) { 24 | // wirebox must be in application scope. 25 | setProperty( "sentryService", application.wirebox.getInstance( "SentryService@sentry" ) ); 26 | } 27 | 28 | return this; 29 | } 30 | 31 | /** 32 | * Log a message 33 | */ 34 | public void function logMessage( required logEvent ){ 35 | var extraInfo = arguments.logEvent.getExtraInfo(); 36 | var level = this.logLevels.lookup( arguments.logEvent.getSeverity() ); 37 | var message = arguments.logEvent.getMessage(); 38 | var loggerCat = arguments.logEvent.getcategory(); 39 | 40 | if ( level == "warn" ) { 41 | level = "warning"; 42 | } 43 | 44 | // Is this an exception or not? 45 | if ( 46 | ( isStruct( extraInfo ) || isObject( extraInfo ) ) 47 | && extraInfo.keyExists( "StackTrace" ) && extraInfo.keyExists( "message" ) && extraInfo.keyExists( 48 | "detail" 49 | ) 50 | ) { 51 | getProperty( "sentryService" ).captureException( 52 | exception = extraInfo, 53 | level = level, 54 | message = message, 55 | logger = loggerCat 56 | ); 57 | } else if ( 58 | ( isStruct( extraInfo ) || isObject( extraInfo ) ) 59 | && extraInfo.keyExists( "exception" ) && isStruct( extraInfo.exception ) && extraInfo.exception.keyExists( 60 | "StackTrace" 61 | ) 62 | ) { 63 | var trimmedExtra = structCopy( extraInfo ); 64 | trimmedExtra.delete( "exception" ); 65 | 66 | getProperty( "sentryService" ).captureException( 67 | exception = extraInfo.exception, 68 | level = level, 69 | message = message, 70 | logger = loggerCat, 71 | additionalData = trimmedExtra 72 | ); 73 | } else { 74 | getProperty( "sentryService" ).captureMessage( 75 | message = message, 76 | level = level, 77 | logger = loggerCat, 78 | additionalData = extraInfo 79 | ); 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /models/SentryService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | ********************************************************************************* 3 | * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | * www.ortussolutions.com 5 | * --- 6 | * Connector to Sentry 7 | */ 8 | component accessors=true singleton { 9 | 10 | // DI 11 | property name="wirebox" inject="wirebox"; 12 | property name="interceptorService" inject="box:InterceptorService"; 13 | property name="functionLineNums" inject="functionLineNums@funclinenums"; 14 | 15 | property name="settings"; 16 | property name="moduleConfig"; 17 | property name="coldbox"; 18 | 19 | property name="levels" type="array"; 20 | 21 | /** The environment name, such as ‘production’ or ‘staging’. */ 22 | property name="environment" type="string"; 23 | /** Default logger name */ 24 | property name="logger" type="string"; 25 | /** Name of platform sending the messages */ 26 | property name="platform" type="string"; 27 | /** The release version of the application. */ 28 | property name="release" type="string"; 29 | /** A DSN string to connect to Sentry's API, the values can also be passed as individual arguments */ 30 | property name="DSN" type="string"; 31 | 32 | /** The ID Sentry Project */ 33 | property name="projectID"; 34 | /** The Public Key for your Sentry Account */ 35 | property name="publicKey"; 36 | /** The Private Key for your Sentry Account */ 37 | property name="privateKey"; 38 | 39 | /** The Sentry API url which defaults to https://sentry.io */ 40 | property name="sentryUrl" type="string"; 41 | /** The Sentry version */ 42 | property name="sentryVersion" type="string"; 43 | /** Which Sentry endpoint to send events to, could be "store" or "envelope" */ 44 | property name="sentryEventEndpoint" type="string"; 45 | /** The name of the server, defaults to machine name, then cgi.http_host */ 46 | property name="serverName" type="string"; 47 | /** Log messages async */ 48 | property name="async" type="boolean"; 49 | /** A UDF that generates user information for logged messages. Returns a struct containing keys "id", "email", "username", and anything else. */ 50 | property name="userInfoUDF"; 51 | /** A dictionary of UDFs to add to the `extra` information. Each function is called and put in the `extra` struct under the provided key. */ 52 | property name="extraInfoUDFs"; 53 | 54 | property name="enabled"; 55 | 56 | // Additional tags 57 | property name="tags"; 58 | 59 | 60 | 61 | /** 62 | * Constructor 63 | */ 64 | function init( struct settings = {} ){ 65 | // make sure coldbox is a true null for Lucee 66 | variables.coldbox = javacast( "null", 0 ); 67 | setSettings( arguments.settings ); 68 | // If we have settings passed to the init, this is likely not 69 | // in WireBox context so just configure now 70 | if ( arguments.settings.count() ) { 71 | configure(); 72 | } 73 | 74 | setModuleConfig( { version : "2.0.0" } ); 75 | 76 | return this; 77 | } 78 | 79 | private function getDefaultSettings(){ 80 | // These are a duplicate of what's in the ModuleConfig. I don't like 81 | // having the here as well, but this is so this service can be used outside 82 | // of ColdBox and not require the ModuleConfig.cfc 83 | return { 84 | // Enable the Sentry LogBox Appender Bridge 85 | "enableLogBoxAppender" : true, 86 | "async" : true, 87 | // Min/Max levels for appender 88 | "levelMin" : "FATAL", 89 | "levelMax" : "ERROR", 90 | // Enable/disable error logging 91 | "enableExceptionLogging" : true, 92 | // Whether to include client cookies when sending request information to Sentry 93 | "sendCookies" : false, 94 | // Whether to include POST data (e.g. FORM) when sending request information to Sentry 95 | "sendPostData" : false, 96 | // Data sanitization, scrub fields and headers, replaced with "[Filtered]" at runtime 97 | "scrubFields" : [ 98 | "passwd", 99 | "password", 100 | "password_confirmation", 101 | "secret", 102 | "confirm_password", 103 | "secret_token", 104 | "APIToken", 105 | "x-api-token", 106 | "fwreinit" 107 | ], 108 | "scrubHeaders" : [ "x-api-token", "Authorization" ], 109 | "release" : "", 110 | "environment" : "production", 111 | "DSN" : "", 112 | "publicKey" : "", 113 | "privateKey" : "", 114 | "projectID" : 0, 115 | "sentryUrl" : "https://sentry.io", 116 | "serverName" : cgi.server_name, 117 | "appRoot" : expandPath( "/" ), 118 | "sentryVersion" : 7, 119 | "sentryEventEndpoint" : "store", 120 | // This is not arbitrary but must be a specific value. Leave as "cfml" 121 | // https://docs.sentry.io/development/sdk-dev/attributes/ 122 | "platform" : "cfml", 123 | "logger" : "sentry", 124 | "userInfoUDF" : "", 125 | "extraInfoUdfs" : {}, 126 | "showJavaStackTrace" : false, 127 | "throwOnPostError" : false 128 | }; 129 | } 130 | 131 | /** 132 | * onDIComplete 133 | */ 134 | function onDIComplete(){ 135 | // If we have WireBox, see if we can get ColdBox 136 | if ( !isNull( wirebox ) ) { 137 | // backwards compat with older versions of ColdBox 138 | if ( wirebox.isColdBoxLinked() ) { 139 | setSettings( wirebox.getInstance( dsl = "coldbox:moduleSettings:sentry" ) ); 140 | setModuleConfig( wirebox.getInstance( dsl = "coldbox:moduleConfig:sentry" ) ); 141 | } else { 142 | // CommandBox supports generic box namespace 143 | setSettings( wirebox.getInstance( dsl = "box:moduleSettings:sentry" ) ); 144 | setModuleConfig( wirebox.getInstance( dsl = "box:moduleConfig:sentry" ) ); 145 | } 146 | 147 | setColdBox( wirebox.getColdBox() ); 148 | } 149 | 150 | configure(); 151 | } 152 | 153 | function configure(){ 154 | setEnabled( true ); 155 | // Add in default settings 156 | settings.append( getDefaultSettings(), false ); 157 | 158 | if ( len( settings.sentryUrl ) ) { 159 | setSentryUrl( settings.sentryUrl ); 160 | } 161 | 162 | if ( len( settings.DSN ) ) { 163 | setDSN( settings.DSN ); 164 | parseDSN( settings.DSN ); 165 | } else if ( len( settings.publicKey ) && len( settings.privateKey ) && len( settings.projectID ) ) { 166 | setPublicKey( settings.publicKey ); 167 | setPrivateKey( settings.privateKey ); 168 | setProjectID( settings.projectID ); 169 | } else { 170 | setPublicKey( "" ); 171 | setPrivateKey( "" ); 172 | setProjectID( 0 ); 173 | setEnabled( false ); 174 | writeDump( 175 | var = "You must configure in a valid DSN or Project Keys and ID to instantiate the Sentry CFML Client.", 176 | output = "console" 177 | ); 178 | } 179 | 180 | setLevels( [ "fatal", "error", "warning", "info", "debug" ] ); 181 | 182 | setRelease( settings.release ); 183 | setEnvironment( settings.environment ); 184 | setServerName( settings.serverName ); 185 | setAsync( settings.async ); 186 | setSentryVersion( settings.sentryVersion ); 187 | setSentryEventEndpoint( settings.sentryEventEndpoint ); 188 | setLogger( settings.logger ); 189 | setPlatform( settings.platform ); 190 | 191 | setUserInfoUDF( settings.userInfoUDF ); 192 | setExtraInfoUDFs( settings.extraInfoUDFs ); 193 | 194 | settings.appRoot = normalizeSlashes( settings.appRoot ); 195 | 196 | // in a non ColdBox context, ensure functionLineNums exists 197 | // so this service can still be used if functionLineNums 198 | // is not passed in 199 | if ( isNull( variables.functionLineNums ) ) { 200 | setFunctionLineNums( { 201 | findTagContextFunction : function(){ 202 | return ""; 203 | } 204 | } ); 205 | } 206 | } 207 | 208 | /** 209 | * Parses a valid Sentry DSN 210 | * 211 | * {PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}{PROJECT_ID} 212 | * or 213 | * {PROTOCOL}://{PUBLIC_KEY}@{HOST}/{PATH}{PROJECT_ID} 214 | * 215 | * https://docs.sentry.io/clientdev/overview/#parsing-the-dsn 216 | */ 217 | private void function parseDSN( required string DSN ){ 218 | var pattern = "^(?:(\w+):)?\/\/(\w+):?(\w+)?@([\w\.\-:]+)\/(.*)"; 219 | var result = reFind( pattern, arguments.DSN, 1, true ); 220 | var segments = []; 221 | 222 | for ( var i = 2; i LTE arrayLen( result.pos ); i++ ) { 223 | // If the secret key is ommitted, the capture group will have a pos and len of 0 224 | if ( result.len[ i ] ) { 225 | segments.append( 226 | mid( 227 | arguments.DSN, 228 | result.pos[ i ], 229 | result.len[ i ] 230 | ) 231 | ); 232 | } 233 | } 234 | 235 | if ( segments.len() == 4 ) { 236 | setSentryUrl( segments[ 1 ] & "://" & segments[ 3 ] ); 237 | setPublicKey( segments[ 2 ] ); 238 | setProjectID( segments[ 4 ] ); 239 | } else if ( segments.len() == 5 ) { 240 | setSentryUrl( segments[ 1 ] & "://" & segments[ 4 ] ); 241 | setPublicKey( segments[ 2 ] ); 242 | setPrivateKey( segments[ 3 ] ); 243 | setProjectID( segments[ 5 ] ); 244 | } else { 245 | throw( message = "Error parsing Sentry DSN" ); 246 | } 247 | } 248 | 249 | /** 250 | * Validates that a correct level was set for a capture 251 | * The allowed levels are: 252 | * "fatal","error","warning","info","debug" 253 | * if you pass "warn", we'll switch it to "warning" 254 | */ 255 | private string function validateLevel( required string level ){ 256 | if ( arguments.level == "warn" ) { 257 | arguments.level = "warning"; 258 | } 259 | 260 | if ( !getLevels().findNoCase( arguments.level ) ) { 261 | throw( 262 | message = "Error Type [#arguments.level#] is invalid. Must be one of the following : " & getLevels().toString() 263 | ); 264 | } 265 | return lCase( arguments.level ); 266 | } 267 | 268 | /** 269 | * Capture a message 270 | * https://develop.sentry.dev/sdk/event-payloads/message/ 271 | * 272 | * @message the raw message string ( max length of 1000 characters ) 273 | * @level The level to log 274 | * @path The path to the script currently executing 275 | * @params an optional list of formatting parameters 276 | * @cgiVars Parameters to send to Sentry, defaults to the CGI Scope 277 | * @useThread Option to send post to Sentry in its own thread 278 | * @userInfo Optional Struct that gets passed to the Sentry User Interface 279 | * @additionalData Additional metadata to store with the event - passed into the extra attribute 280 | * @tags Optional. A struct of tags for this event. Each tag must be less than 200 characters. 281 | * @fingerprint Optional An array of strings used to dictate the deduplication of this event. 282 | */ 283 | public any function captureMessage( 284 | required string message, 285 | string level = "info", 286 | string path = "", 287 | array params, 288 | any cgiVars = cgi, 289 | boolean useThread = getAsync(), 290 | struct userInfo = {}, 291 | string logger = getLogger(), 292 | any additionalData, 293 | struct tags = {}, 294 | array fingerprint = [] 295 | ){ 296 | if ( !getEnabled() ) { 297 | return; 298 | } 299 | var sentryMessage = {}; 300 | 301 | arguments.level = validateLevel( arguments.level ); 302 | 303 | if ( len( trim( arguments.message ) ) > 1000 ) arguments.message = left( arguments.message, 997 ) & "..."; 304 | 305 | sentryMessage = { 306 | "message" : arguments.message, 307 | "level" : arguments.level, 308 | "logentry" : { "formatted" : arguments.message }, 309 | "logger" : arguments.logger 310 | }; 311 | 312 | if ( structKeyExists( arguments, "params" ) ) { 313 | sentryMessage[ "logentry" ][ "params" ] = arguments.params; 314 | } 315 | // Add tags 316 | if ( !structIsEmpty( arguments.tags ) ) { 317 | sentryMessage[ "tags" ] = arguments.tags; 318 | } 319 | 320 | // Add fingerprint 321 | if ( arrayLen( arguments.fingerprint ) ) { 322 | sentryMessage[ "fingerprint" ] = arguments.fingerprint; 323 | } 324 | 325 | 326 | if ( !isNull( additionalData ) ) { 327 | if ( !isStruct( additionalData ) ) { 328 | additionalData = { "Additional Data" : additionalData }; 329 | } 330 | sentryMessage[ "extra" ] = additionalData; 331 | } 332 | 333 | capture( 334 | captureStruct: sentryMessage, 335 | path : arguments.path, 336 | cgiVars : arguments.cgiVars, 337 | useThread : arguments.useThread, 338 | userInfo : arguments.userInfo 339 | ); 340 | } 341 | 342 | /** 343 | * @exception The exception 344 | * @level The level to log 345 | * @path The path to the script currently executing 346 | * @oneLineStackTrace Set to true to render only 1 tag context. This is not the Java Stack Trace this is simply for the code output in Sentry 347 | * @showJavaStackTrace Passes Java Stack Trace as a string to the extra attribute 348 | * @removeTabsOnJavaStackTrace Removes the tab on the child lines in the Stack Trace 349 | * @additionalData Additional metadata to store with the event - passed into the extra attribute 350 | * @cgiVars Parameters to send to Sentry, defaults to the CGI Scope 351 | * @useThread Option to send post to Sentry in its own thread 352 | * @userInfo Optional Struct that gets passed to the Sentry User Interface 353 | * @message Optional message name to output 354 | * @logger Optional logger to use 355 | * @tags Optional. A struct of tags for this event. Each tag must be less than 200 characters. 356 | * @fingerprint Optional An array of strings used to dictate the deduplication of this event. 357 | */ 358 | public any function captureException( 359 | required any exception, 360 | string level = "error", 361 | string path = "", 362 | boolean oneLineStackTrace = false, 363 | boolean showJavaStackTrace = settings.showJavaStackTrace, 364 | boolean removeTabsOnJavaStackTrace = false, 365 | any additionalData, 366 | any cgiVars = cgi, 367 | boolean useThread = getAsync(), 368 | struct userInfo = {}, 369 | string message = "", 370 | string logger = getLogger(), 371 | struct tags = {}, 372 | array fingerprint = [] 373 | ){ 374 | if ( !getEnabled() ) { 375 | return; 376 | } 377 | 378 | // Ensure expected keys exist 379 | arguments.exception.StackTrace = arguments.exception.StackTrace ?: ""; 380 | arguments.exception.type = arguments.exception.type ?: ""; 381 | arguments.exception.detail = arguments.exception.detail ?: ""; 382 | arguments.exception.TagContext = arguments.exception.TagContext ?: []; 383 | arguments.exception.message = arguments.exception.message ?: ""; 384 | 385 | var sentryExceptionExtra = {}; 386 | var file = ""; 387 | var fileArray = ""; 388 | var currentTemplate = ""; 389 | var tagContext = arguments.exception.TagContext; 390 | var i = 1; 391 | var st = ""; 392 | 393 | // If there's no tag context, include the stack trace instead 394 | if ( !tagContext.len() ) { 395 | arguments.showJavaStackTrace = true; 396 | } 397 | 398 | arguments.level = validateLevel( arguments.level ); 399 | 400 | /* 401 | * CORE AND OPTIONAL ATTRIBUTES 402 | * https://develop.sentry.dev/sdk/event-payloads/ 403 | */ 404 | var sentryException = { 405 | "level" : arguments.level, 406 | "logger" : arguments.logger, 407 | "message" : arguments.exception.message & " " & arguments.exception.detail 408 | }; 409 | 410 | 411 | // Add tags 412 | if ( !structIsEmpty( arguments.tags ) ) { 413 | sentryException[ "tags" ] = arguments.tags; 414 | } 415 | 416 | // Add fingerprint 417 | if ( arrayLen( arguments.fingerprint ) ) { 418 | sentryException[ "fingerprint" ] = arguments.fingerprint; 419 | } 420 | 421 | if ( arguments.message != arguments.exception.message ) { 422 | sentryException.message = arguments.message & " " & sentryException.message; 423 | } 424 | 425 | if ( arguments.showJavaStackTrace ) { 426 | st = reReplace( 427 | arguments.exception.StackTrace, 428 | "\r", 429 | "", 430 | "All" 431 | ); 432 | if ( arguments.removeTabsOnJavaStackTrace ) st = reReplace( st, "\t", "", "All" ); 433 | sentryExceptionExtra[ "Java StackTrace" ] = listToArray( st, chr( 10 ) ); 434 | } 435 | 436 | if ( !isNull( arguments.additionalData ) ) { 437 | sentryExceptionExtra[ "Additional Data" ] = arguments.additionalData; 438 | } 439 | 440 | // Applies to type = "database". Native error code associated with exception. Database drivers typically provide error codes to diagnose failing database operations. Default value is -1. 441 | if ( structKeyExists( arguments.exception, "NativeErrorCode" ) ) { 442 | sentryExceptionExtra[ "DataBase" ][ "NativeErrorCode" ] = arguments.exception.NativeErrorCode; 443 | } 444 | 445 | // Applies to type = "database". SQLState associated with exception. Database drivers typically provide error codes to help diagnose failing database operations. Default value is 1. 446 | if ( structKeyExists( arguments.exception, "SQLState" ) ) { 447 | sentryExceptionExtra[ "DataBase" ][ "SQL State" ] = arguments.exception.SQLState; 448 | } 449 | 450 | // Applies to type = "database". The SQL statement sent to the data source. 451 | if ( structKeyExists( arguments.exception, "Sql" ) ) { 452 | sentryExceptionExtra[ "DataBase" ][ "SQL" ] = arguments.exception.Sql; 453 | } 454 | 455 | // Applies to type ="database". The error message as reported by the database driver. 456 | if ( structKeyExists( arguments.exception, "queryError" ) ) { 457 | sentryExceptionExtra[ "DataBase" ][ "Query Error" ] = arguments.exception.queryError; 458 | } 459 | 460 | // Applies to type= "database". If the query uses the cfqueryparam tag, query parameter name-value pairs. 461 | if ( structKeyExists( arguments.exception, "where" ) ) { 462 | sentryExceptionExtra[ "DataBase" ][ "Where" ] = arguments.exception.where; 463 | } 464 | 465 | // Applies to type = "expression". Internal expression error number. 466 | if ( structKeyExists( arguments.exception, "ErrNumber" ) ) { 467 | sentryExceptionExtra[ "expression" ][ "Error Number" ] = arguments.exception.ErrNumber; 468 | } 469 | 470 | // Applies to type = "missingInclude". Name of file that could not be included. 471 | if ( structKeyExists( arguments.exception, "MissingFileName" ) ) { 472 | sentryExceptionExtra[ "missingInclude" ][ "Missing File Name" ] = arguments.exception.MissingFileName; 473 | } 474 | 475 | // Applies to type = "lock". Name of affected lock (if the lock is unnamed, the value is "anonymous"). 476 | if ( structKeyExists( arguments.exception, "LockName" ) ) { 477 | sentryExceptionExtra[ "lock" ][ "Lock Name" ] = arguments.exception.LockName; 478 | } 479 | 480 | // Applies to type = "lock". Operation that failed (Timeout, Create Mutex, or Unknown). 481 | if ( structKeyExists( arguments.exception, "LockOperation" ) ) { 482 | sentryExceptionExtra[ "lock" ][ "Lock Operation" ] = arguments.exception.LockOperation; 483 | } 484 | 485 | // Applies to type = "custom". String error code. 486 | if ( 487 | structKeyExists( arguments.exception, "ErrorCode" ) && len( arguments.exception.ErrorCode ) && arguments.exception.ErrorCode != "0" 488 | ) { 489 | sentryExceptionExtra[ "custom" ][ "Error Code" ] = arguments.exception.ErrorCode; 490 | } 491 | 492 | // Applies to type = "application" and "custom". Custom error message; information that the default exception handler does not display. 493 | if ( structKeyExists( arguments.exception, "ExtendedInfo" ) && len( arguments.exception.ExtendedInfo ) ) { 494 | sentryExceptionExtra[ "application" ][ "Extended Info" ] = isJSON( arguments.exception.ExtendedInfo ) ? deserializeJSON( 495 | arguments.exception.ExtendedInfo 496 | ) : arguments.exception.ExtendedInfo; 497 | } 498 | 499 | if ( structCount( sentryExceptionExtra ) ) sentryException[ "extra" ] = sentryExceptionExtra; 500 | 501 | /* 502 | * EXCEPTION INTERFACE 503 | * https://https://develop.sentry.dev/sdk/event-payloads/exception/ 504 | */ 505 | var currentException = { 506 | "value" : arguments.exception.message & " " & arguments.exception.detail, 507 | "type" : arguments.exception.type & " Error", 508 | "stacktrace" : { "frames" : [] } 509 | }; 510 | 511 | sentryException[ "exception" ] = { "values" : [ currentException ] }; 512 | 513 | 514 | 515 | /* 516 | * STACKTRACE INTERFACE 517 | * https://develop.sentry.dev/sdk/event-payloads/stacktrace/ 518 | */ 519 | if ( arguments.oneLineStackTrace ) { 520 | tagContext = [ tagContext[ 1 ] ]; 521 | } 522 | 523 | var stacki = 0; 524 | for ( i = arrayLen( tagContext ); i > 0; i-- ) { 525 | stacki++; 526 | var thisTCItem = tagContext[ i ]; 527 | if ( compareNoCase( thisTCItem[ "TEMPLATE" ], currentTemplate ) ) { 528 | fileArray = []; 529 | if ( fileExists( thisTCItem[ "TEMPLATE" ] ) ) { 530 | file = fileOpen( thisTCItem[ "TEMPLATE" ], "read" ); 531 | while ( !fileIsEOF( file ) ) { 532 | arrayAppend( fileArray, fileReadLine( file ) ); 533 | } 534 | fileClose( file ); 535 | } 536 | currentTemplate = thisTCItem[ "TEMPLATE" ]; 537 | } 538 | 539 | var thisStackItem = { 540 | "abs_path" : thisTCItem[ "TEMPLATE" ], 541 | "filename" : normalizeSlashes( thisTCItem[ "TEMPLATE" ] ).replace( variables.settings.appRoot, "" ), 542 | "lineno" : thisTCItem[ "LINE" ], 543 | "pre_context" : [], 544 | "context_line" : "", 545 | "post_context" : [] 546 | }; 547 | 548 | // The name of the function being called 549 | var functionName = functionLineNums.findTagContextFunction( thisTCItem ); 550 | if ( len( functionName ) ) { 551 | thisStackItem[ "function" ] = functionName; 552 | } 553 | 554 | // for source code rendering 555 | var fileLen = arrayLen( fileArray ); 556 | var errorLine = thisTCItem[ "LINE" ]; 557 | 558 | if ( errorLine - 3 >= 1 && errorLine - 3 <= fileLen ) { 559 | thisStackItem.pre_context[ 1 ] = fileArray[ errorLine - 3 ]; 560 | } 561 | if ( errorLine - 2 >= 1 && errorLine - 2 <= fileLen ) { 562 | thisStackItem.pre_context[ 2 ] = fileArray[ errorLine - 2 ]; 563 | } 564 | if ( errorLine - 1 >= 1 && errorLine - 1 <= fileLen ) { 565 | thisStackItem.pre_context[ 3 ] = fileArray[ errorLine - 1 ]; 566 | } 567 | 568 | if ( errorLine <= fileLen && fileLen > 0 && errorLine >= 1 ) { 569 | thisStackItem[ "context_line" ] = fileArray[ errorLine ]; 570 | } 571 | 572 | if ( fileLen >= errorLine + 1 ) { 573 | var errorLine1 = errorLine + 1; 574 | 575 | if ( errorLine1 != 0 ) { 576 | thisStackItem.post_context[ 1 ] = fileArray[ errorLine1 ]; 577 | } else if ( fileLen >= errorLine1 + 1 ) { 578 | thisStackItem.post_context[ 1 ] = fileArray[ errorLine1 + 1 ]; 579 | } 580 | } 581 | 582 | if ( fileLen >= errorLine + 2 ) { 583 | var errorLine2 = errorLine + 2; 584 | 585 | if ( errorLine2 != 1 ) { 586 | thisStackItem.post_context[ 2 ] = fileArray[ errorLine2 ]; 587 | } else if ( fileLen >= errorLine2 + 1 ) { 588 | thisStackItem.post_context[ 2 ] = fileArray[ errorLine2 + 1 ]; 589 | } 590 | } 591 | 592 | currentException[ "stacktrace" ][ "frames" ][ stacki ] = thisStackItem; 593 | } 594 | 595 | capture( 596 | captureStruct: sentryException, 597 | path : arguments.path, 598 | cgiVars : arguments.cgiVars, 599 | useThread : arguments.useThread, 600 | userInfo : arguments.userInfo 601 | ); 602 | } 603 | 604 | // recursivley replace any CFC instances with structs 605 | function structifyObject( o, name = "" ){ 606 | var result = {}; 607 | if ( len( name ) ) { 608 | // Find a way to communicate what the original CFC instance was, even though it's a struct now 609 | result[ "__componentName" ] = name; 610 | } 611 | return structReduce( 612 | o, 613 | function( acc, k, v ){ 614 | if ( !isCustomFunction( v ) ) { 615 | if ( isObject( v ) ) { 616 | acc[ k ] = structifyObject( v, getMetadata( v ).name ); 617 | } else if ( isStruct( v ) ) { 618 | acc[ k ] = structifyObject( v ); 619 | } else { 620 | acc[ k ] = v; 621 | } 622 | } 623 | return acc; 624 | }, 625 | result 626 | ); 627 | } 628 | 629 | /** 630 | * Prepare message to post to Sentry 631 | * 632 | * @captureStruct The struct we are passing to Sentry 633 | * @cgiVars Parameters to send to Sentry, defaults to the CGI Scope 634 | * @path The path to the script currently executing 635 | * @useThread Option to send post to Sentry in its own thread 636 | * @userInfo Optional Struct that gets passed to the Sentry User Interface 637 | */ 638 | public void function capture( 639 | required any captureStruct, 640 | any cgiVars = cgi, 641 | string path = "", 642 | boolean useThread = getAsync(), 643 | struct userInfo = {} 644 | ){ 645 | var jsonCapture = ""; 646 | var signature = ""; 647 | var header = ""; 648 | var timeVars = getTimeVars(); 649 | var httpRequestData = getHTTPDataForRequest(); 650 | var traceParent = httpRequestData.headers.traceParent ?: ""; 651 | 652 | // Add global metadata 653 | arguments.captureStruct[ "event_id" ] = lCase( replace( createUUID(), "-", "", "all" ) ); 654 | arguments.captureStruct[ "timestamp" ] = timeVars.iso; 655 | arguments.captureStruct[ "project" ] = getProjectID(); 656 | arguments.captureStruct[ "server_name" ] = getServerName(); 657 | arguments.captureStruct[ "platform" ] = getPlatform(); 658 | arguments.captureStruct[ "release" ] = getRelease(); 659 | arguments.captureStruct[ "environment" ] = getEnvironment(); 660 | 661 | /* 662 | * User interface 663 | * https://develop.sentry.dev/sdk/event-payloads/user/ 664 | * 665 | * { 666 | * "id" : "unique_id", 667 | * "email" : "foo@example.com", 668 | * "username" : ""my_user", 669 | * "ip_address" : "127.0.0.1" 670 | * } 671 | * 672 | * All other keys are stored as extra information but not specifically processed by sentry. 673 | */ 674 | var thisUserInfo = { "ip_address" : getRealIP() }; 675 | 676 | var userInfoUDF = getUserInfoUDF(); 677 | // If there is a closure to produce user info, call it 678 | if ( isCustomFunction( userInfoUDF ) ) { 679 | // Check for a non-ColdBox context 680 | if ( isNull( coldbox ) ) { 681 | // Call the custon closure to produce user info 682 | local.tmpUserInfo = userInfoUDF(); 683 | } else { 684 | // Prepare the request context for the closure to use 685 | var event = coldbox.getRequestService().getContext(); 686 | // Call the custon closure to produce user info 687 | local.tmpUserInfo = userInfoUDF( 688 | event, 689 | event.getCollection(), 690 | event.getPrivateCollection() 691 | ); 692 | } 693 | 694 | // Assemble any trace parent data from coldbox 695 | if ( !len( traceParent ) && !isNull( coldbox ) ) { 696 | // Append any trace information which might be provided the `cbotel` module 697 | var prc = coldbox 698 | .getRequestService() 699 | .getContext() 700 | .getPrivateCollection(); 701 | if ( prc.keyExists( "openTelemetry" ) && isStruct( prc.openTelemetry ) ) { 702 | traceParent = prc.openTelemetry.traceParent ?: ""; 703 | } 704 | } 705 | 706 | if ( !isNull( local.tmpUserInfo ) && isStruct( local.tmpUserInfo ) ) { 707 | thisUserInfo.append( local.tmpUserInfo ); 708 | } 709 | } 710 | if ( !arguments.userInfo.isEmpty() ) { 711 | thisUserInfo.append( arguments.userInfo ); 712 | } 713 | 714 | // Force lowercasing on these since Sentry looks for them 715 | // Stupid CF won't udpate key casing in-place, so creating a new struct. 716 | var correctCasingUserInfo = {}; 717 | for ( var key in thisUserInfo ) { 718 | if ( listFindNoCase( "id,email,ip_address,username", key ) ) { 719 | key = lCase( key ); 720 | } 721 | correctCasingUserInfo[ key ] = thisUserInfo[ key ]; 722 | } 723 | 724 | arguments.captureStruct[ "user" ] = correctCasingUserInfo; 725 | 726 | var extraInfoUdfs = getExtraInfoUdfs(); 727 | for ( var key in extraInfoUdfs ) { 728 | var extraInfoUdf = extraInfoUdfs[ key ]; 729 | arguments.captureStruct[ "extra" ][ key ] = extraInfoUdf(); 730 | } 731 | 732 | // Prepare path for Request Interface 733 | arguments.path = trim( arguments.path ); 734 | if ( !len( arguments.path ) && structCount( arguments.cgiVars ) ) { 735 | // leave off script name for SES URLs since rewrites were probably used 736 | if ( arguments.cgiVars.script_name == "/index.cfm" && len( arguments.cgiVars.path_info ) ) { 737 | arguments.path = "http" & ( arguments.cgiVars.server_port_secure ? "s" : "" ) & "://" & arguments.cgiVars.server_name & arguments.cgiVars.path_info; 738 | } else { 739 | arguments.path = "http" & ( arguments.cgiVars.server_port_secure ? "s" : "" ) & "://" & arguments.cgiVars.server_name & arguments.cgiVars.script_name & arguments.cgiVars.path_info; 740 | } 741 | } 742 | 743 | // Request interface 744 | // https://develop.sentry.dev/sdk/event-payloads/request/ 745 | arguments.captureStruct[ "request" ] = { 746 | "url" : arguments.path, 747 | "method" : arguments.cgiVars.request_method ?: "GET", 748 | "query_string" : sanitizeQueryString( arguments.cgiVars.query_string ?: "" ), 749 | "env" : sanitizeEnv( arguments.cgiVars ), 750 | "headers" : sanitizeHeaders( httpRequestData.headers ) 751 | }; 752 | 753 | if ( variables.settings.sendCookies ) { 754 | arguments.captureStruct[ "request" ][ "cookies" ] = sanitizeFields( 755 | cookie.map( function( k, v ){ 756 | return toString( v ); // Sentry requires all cookies be strings 757 | } ) 758 | ); 759 | } 760 | 761 | if ( variables.settings.sendPostData ) { 762 | if ( !structIsEmpty( form ) ) { 763 | arguments.captureStruct[ "request" ][ "data" ] = sanitizeFields( form ); 764 | } else { 765 | arguments.captureStruct[ "request" ][ "data" ] = sanitizeFields( 766 | isJSON( httpRequestData.content ) ? deserializeJSON( httpRequestData.content ) : {} 767 | ); 768 | } 769 | } 770 | 771 | // Announce an interception to allow other modules and listeners to modify the sentry request 772 | if ( !isNull( coldbox ) ) { 773 | getInterceptorService().announce( "onSentryEventCapture", { "event" : arguments.captureStruct } ); 774 | } 775 | 776 | // serialize data 777 | jsonCapture = serializeJSON( arguments.captureStruct ); 778 | 779 | // prepare header 780 | // https://develop.sentry.dev/sdk/overview/#authentication 781 | header = "Sentry sentry_version=#getSentryVersion()#, sentry_client=Sentry/#moduleConfig.version#, sentry_key=#getPublicKey()#"; 782 | if ( getSentryEventEndpoint() == "store" ) { 783 | header &= ", sentry_timestamp=#timeVars.unix#"; 784 | } 785 | if ( !isNull( getPrivateKey() ) && len( getPrivateKey() ) ) { 786 | header &= ", sentry_secret=#getPrivateKey()#"; 787 | } 788 | 789 | // post message 790 | if ( arguments.useThread ) { 791 | cfthread( 792 | action = "run", 793 | name = "sentry-thread-" & createUUID(), 794 | header = header, 795 | event_id = captureStruct.event_id, 796 | sent_at = timeVars.iso, 797 | jsonCapture = jsonCapture, 798 | traceParent = traceParent 799 | ) { 800 | post( 801 | header, 802 | event_id, 803 | sent_at, 804 | jsonCapture, 805 | traceParent 806 | ); 807 | } 808 | } else { 809 | post( 810 | header, 811 | captureStruct.event_id, 812 | timeVars.iso, 813 | jsonCapture, 814 | traceParent 815 | ); 816 | } 817 | } 818 | 819 | public SentryService function addExtraInfoUdf( required string key, required function udf ){ 820 | variables.extraInfoUDFs[ arguments.key ] = arguments.udf; 821 | return this; 822 | } 823 | 824 | /** 825 | * Post message to Sentry 826 | */ 827 | private void function post( 828 | required string header, 829 | required string event_id, 830 | required string sent_at, 831 | required string json, 832 | string traceParent = "" 833 | ){ 834 | var http = {}; 835 | // send to sentry via REST API Call 836 | var httpBody = arguments.json; 837 | 838 | if ( getSentryEventEndpoint() == "envelope" ) { 839 | var envelope = []; 840 | 841 | arrayAppend( 842 | envelope, 843 | serializeJSON( { 844 | "event_id" : arguments.event_id, 845 | "sent_at" : arguments.sent_at 846 | } ) 847 | ); 848 | arrayAppend( 849 | envelope, 850 | serializeJSON( { 851 | "type" : "event", 852 | "length" : len( arguments.json ), 853 | "content_type" : "application/json" 854 | } ) 855 | ); 856 | arrayAppend( envelope, arguments.json ); 857 | 858 | 859 | httpBody = arrayToList( envelope, chr( 10 ) ) & chr( 10 ); 860 | } 861 | 862 | cfhttp( 863 | url = getSentryUrl() & "/api/" & getProjectID() & "/" & getSentryEventEndpoint() & "/", 864 | method = "post", 865 | timeout = "2", 866 | result = "http" 867 | ) { 868 | cfhttpparam( 869 | type = "header", 870 | name = "X-Sentry-Auth", 871 | value = arguments.header 872 | ); 873 | 874 | // Add our traceparent header if provided https://develop.sentry.dev/sdk/telemetry/traces/#header-traceparent 875 | if ( len( arguments.traceparent ) ) { 876 | cfhttpparam( 877 | type = "header", 878 | name = "traceparent", 879 | value = arguments.traceparent 880 | ); 881 | } 882 | 883 | cfhttpparam( type = "body", value = httpBody ); 884 | } 885 | 886 | if ( find( "400", http.statuscode ) || find( "500", http.statuscode ) || !find( "200", http.statuscode ) ) { 887 | if ( settings.throwOnPostError ) { 888 | throw( message = "Error posting to Sentry: #http.statuscode#", detail = http.filecontent ); 889 | } else { 890 | writeDump( 891 | var = "Error posting to Sentry: #http.statuscode# - #left( http.filecontent, 1000 )#", 892 | output = "console" 893 | ); 894 | } 895 | // TODO : Honor Sentry’s HTTP 429 Retry-After header any other errors 896 | } 897 | } 898 | 899 | /** 900 | * Get UTC time values 901 | */ 902 | private struct function getTimeVars(){ 903 | var time = now(); 904 | var timeVars = { 905 | "unix" : toString( int( time.getTime() / 1000 ) ), 906 | "iso" : dateTimeFormat( time, "yyyy-mm-dd'T'HH:nn:ss'Z'", "UTC" ) 907 | }; 908 | return timeVars; 909 | } 910 | 911 | /** 912 | * Get the host name you are on 913 | */ 914 | private function getHostName(){ 915 | try { 916 | return createObject( "java", "java.net.InetAddress" ).getLocalHost().getHostName(); 917 | } catch ( Any e ) { 918 | return cgi.http_host; 919 | } 920 | } 921 | 922 | /** 923 | * Get Real IP, by looking at clustered, proxy headers and locally. 924 | */ 925 | private function getRealIP(){ 926 | var headers = getHTTPDataForRequest().headers; 927 | 928 | // When going through a proxy, the IP can be a delimtied list, thus we take the last one in the list 929 | 930 | if ( structKeyExists( headers, "x-cluster-client-ip" ) ) { 931 | return trim( listLast( headers[ "x-cluster-client-ip" ] ) ); 932 | } 933 | if ( structKeyExists( headers, "X-Forwarded-For" ) ) { 934 | return trim( listFirst( headers[ "X-Forwarded-For" ] ) ); 935 | } 936 | 937 | return len( cgi.remote_addr ) ? trim( listFirst( cgi.remote_addr ) ) : "127.0.0.1"; 938 | } 939 | 940 | 941 | /** 942 | * Sanitize the incoming http headers in the request data struct 943 | * 944 | * @data The HTTP data struct, passed by reference 945 | */ 946 | private function sanitizeHeaders( required struct headers ){ 947 | if ( structCount( arguments.headers ) ) { 948 | for ( var thisHeader in variables.settings.scrubHeaders ) { 949 | // If header found, then sanitize it. 950 | if ( structKeyExists( arguments.headers, thisHeader ) ) { 951 | arguments.headers[ thisHeader ] = "[Filtered]"; 952 | } 953 | } 954 | } 955 | 956 | if ( !variables.settings.sendCookies && structKeyExists( arguments.headers, "Cookie" ) ) { 957 | arguments.headers.Cookie = "[Filtered]"; 958 | } 959 | 960 | // Sentry requires all headers be strings 961 | return arguments.headers.map( function( k, v ){ 962 | return toString( v ); 963 | } ); 964 | } 965 | 966 | /** 967 | * Sanitize fields 968 | * 969 | * @data The data fields 970 | */ 971 | private any function sanitizeFields( required any data ){ 972 | if ( !isStruct( arguments.data ) ) { 973 | return arguments.data; 974 | } 975 | if ( structCount( arguments.data ) ) { 976 | for ( var thisField in variables.settings.scrubFields ) { 977 | // If field found, then sanitize it. 978 | if ( structKeyExists( arguments.data, thisField ) ) { 979 | arguments.data[ thisField ] = "[Filtered]"; 980 | } 981 | } 982 | } 983 | return arguments.data; 984 | } 985 | 986 | /** 987 | * Sanitize env/CGI vars 988 | * 989 | * @data The data fields 990 | */ 991 | private any function sanitizeEnv( required any data ){ 992 | if ( !isStruct( arguments.data ) ) { 993 | return arguments.data; 994 | } 995 | 996 | // don't mutate CGI scope 997 | return arguments.data.map( function( k, v ){ 998 | if ( !variables.settings.sendCookies && k == "http_cookie" ) { 999 | return "[Filtered]"; 1000 | } 1001 | return v; 1002 | } ); 1003 | } 1004 | 1005 | /** 1006 | * Sanitize the incoming query string 1007 | * 1008 | * @target The target string to sanitize 1009 | */ 1010 | private function sanitizeQueryString( required string target ){ 1011 | var aTarget = listToArray( target, "&" ).map( function( item, index, array ){ 1012 | var key = listFirst( arguments.item, "=" ); 1013 | var value = listLen( arguments.item, "=" GT 1 ) ? listLast( arguments.item, "=" ) : ""; 1014 | 1015 | // Sanitize? 1016 | if ( arrayContainsNoCase( variables.settings.scrubFields, key ) ) { 1017 | value = "[Filtered]"; 1018 | } 1019 | 1020 | return "#key#=#value#"; 1021 | } ); 1022 | return arrayToList( aTarget, "&" ); 1023 | } 1024 | 1025 | /** 1026 | * Turns all slashes in a path to forward slashes except for \\ in a Windows UNC network share 1027 | * Also changes double slashes to a single slash 1028 | * 1029 | * @path The path to normalize 1030 | */ 1031 | function normalizeSlashes( string path ){ 1032 | var normalizedPath = arguments.path.replace( "\", "/", "all" ); 1033 | if ( arguments.path.left( 2 ) == "\\" ) { 1034 | normalizedPath = "\\" & normalizedPath.mid( 3, normalizedPath.len() - 2 ); 1035 | } 1036 | return normalizedPath.replace( "//", "/", "all" ); 1037 | } 1038 | 1039 | /** 1040 | * I return the http request data 1041 | */ 1042 | struct function getHTTPDataForRequest(){ 1043 | try { 1044 | var result = getHTTPRequestData(); 1045 | if ( !isNull( result ) ) { 1046 | return result; 1047 | } 1048 | } catch ( any e ) { 1049 | } 1050 | return { "headers" : {}, "content" : "" }; 1051 | } 1052 | 1053 | } 1054 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/coldbox-modules/sentry.svg?branch=development)](https://travis-ci.org/coldbox-modules/sentry) 2 | 3 | # Welcome to the Sentry Module 4 | 5 | This module connects your CFML application to send bug reports to Sentry (https://sentry.io) 6 | 7 | ## LICENSE 8 | 9 | Apache License, Version 2.0. 10 | 11 | ## IMPORTANT LINKS 12 | 13 | - Source: https://github.com/coldbox-modules/sentry 14 | - Issues: https://github.com/coldbox-modules/sentry/issues 15 | - Account Setup: https://sentry.io 16 | - [Changelog](changelog.md) 17 | 18 | ## SYSTEM REQUIREMENTS 19 | 20 | - Adobe ColdFusion 2016+ 21 | - Lucee 5 22 | 23 | ## Instructions 24 | 25 | Just drop into your modules folder or use the `box` cli to install 26 | 27 | ```bash 28 | box install sentry 29 | ``` 30 | 31 | ## Updating to Version 2 32 | 33 | Version 2 of this module includes some potentially breaking changes in how the event data that is sent to Sentry is constructed. In version 2 the events match the up to date format that Sentry has adopted for their official SDKs. If you are not sending your events to the Sentry hosted service, but using a self-hosted Sentry instance, please be sure you are running an up to date version of the Sentry service before updating to version 2 of this module. 34 | 35 | Additionally, Sentry has deprecated sending events to the `/api/{project_id}/store` endpoint in favor of a new `/api/{project_id}/envelope` endpoint (and a new structure for the body of the post requests). Again, if you self-host an older version of the Sentry service, sending events to the `store` endpoint might be your only option. There is a new module setting, `sentryEventEndpoint`, that defaults to `store`, but can be set to `envelope` to enable sending events to the modern endpoint. 36 | 37 | In version 2, `cookie` and `form` scope data will not be sent with events by default. To enable sending this data, use the new `sendCookies` and `sendPostData` settings. 38 | 39 | ## CFML App Installation 40 | 41 | If your app uses neither ColdBox nor LogBox, you can still instantiate the `SentryService` and use it directly so long as you prep it with the settings it needs. 42 | 43 | ```js 44 | // Create Sentry service and load it with data 45 | application.sentryService = new modules.sentry.models.SentryService( { 46 | async : true, 47 | DSN : 'https://xxxxxxxxxx@sentry.io/3' 48 | } ); 49 | 50 | // Send a log message 51 | application.sentryService.captureMessage( 'winter is coming', 'warning' ); 52 | 53 | // Send an exception 54 | application.sentryService.captureException( exception=cfcatch, additionalData={ anything : 'here' } ); 55 | ``` 56 | 57 | This module makes use of the `funclinenums` module for the purpose of computing and reporting CFML function names in a stack trace. If you installed this via CommandBox, `funclinenums` was installed as a dependency for you. In a ColdBox app you don't need to do anything more, as WireBox will take care of wiring it up for you. However, in a non ColdBox app, if you want CFML function names reported in your stack trace you will need to add it to the `SentryService` yourself. 58 | 59 | ```cfc 60 | functionLineNums = new modules.sentry.modules.funclinenums.functionLineNums(); 61 | application.sentryService.setFunctionLineNums(functionLineNums); 62 | ``` 63 | 64 | ## LogBox Standalone Installation 65 | 66 | If your app doesn't use ColdBox but does use LogBox, you can use our `SentryAppender` class in your LogBox config. You'll need to still instantiate the `SentryService` the same as above, but then you can just use the standard LogBox API to send your messages. 67 | 68 | This means if your app already has LogBox calls in place, simply adding the Sentry appender will start sending all those messages to Sentry without any app code changes on your part. 69 | 70 | Here is an example LogBox standalone config file 71 | 72 | **MyLogBoxConfig.cfc** 73 | ```js 74 | component { 75 | function configure() { 76 | logBox = { 77 | appenders : { 78 | sentry : { 79 | class : 'modules.sentry.models.SentryAppender', 80 | levelMax : 'WARN', 81 | properties : { 82 | sentryService : new sentry.models.SentryService( { 83 | async : true, 84 | DSN : 'https://xxxxxxxxxx@sentry.io/3' 85 | } ) 86 | } 87 | } 88 | }, 89 | root : { levelMax : 'INFO', appenders : '*' }, 90 | categories = {} 91 | }; 92 | } 93 | } 94 | ``` 95 | 96 | Then create LogBox as normal and send your messages: 97 | ```js 98 | application.logbox = new logbox.system.logging.LogBox( 99 | new logbox.system.logging.config.LogBoxConfig( CFCConfigPath="config.MyLogBoxConfig" ) 100 | ); 101 | 102 | // Send a log message 103 | application.logbox.getRootLogger().warn( 'winter is coming' ); 104 | 105 | // Send an exception 106 | application.logbox.getRootLogger().error( message='Boom boom', extraInfo=cfcatch ); 107 | 108 | // Send an exception plus other stuff 109 | application.logbox.getRootLogger().error( message='Boom boom', extraInfo={ exception: cfcatch, anything: 'here', as : 'well' ); 110 | ``` 111 | 112 | The `extraInfo` is optional, but if it is a cfcatch object or a struct containing a cfcatch object in a key called `exception`, the appender will use special treatment of the exception object. Both of the examples above will extract the cfcatch and log to Sentry as an error which contain additional information over a simple text log message. 113 | 114 | ## ColdBox Installation 115 | 116 | Lucky you, ColdBox provides you with the "easy street" method of using this module. By just installing the module, the following things will happen automatically for you: 117 | * The Sentry LogBox appender we showed above will be registered to capture all messages of FATAL or ERROR severity 118 | * An `onException` interceptor will be registered to log all errors that ColdBox sees. 119 | 120 | The only required configuration is your client DSN or auth keys so we can contact Sentry. This configuration goes in `/config/ColdBox.cfc` in `moduleSettings.sentry` like so: 121 | 122 | ```js 123 | moduleSettings = { 124 | sentry : { 125 | async : true, 126 | DSN : 'https://xxxxxxxxxx@sentry.io/3' 127 | } 128 | }; 129 | ``` 130 | 131 | ## Settings 132 | 133 | Regardless of the installation method above, the settings for Sentry are mostly the same. Here is the full list. Note, `enableLogBoxAppender`, `levelMin`, `levelMax`, and `enableExceptionLogging` are only used in when installing Sentry into a ColdBox app. 134 | The default values are shown below. Any settings you omit will use the default values. 135 | 136 | ```js 137 | settings = { 138 | // Enable the Sentry LogBox Appender Bridge 139 | enableLogBoxAppender : true, 140 | // Min/Max levels for appender 141 | levelMin : 'FATAL', 142 | levelMax : 'ERROR', 143 | // auto-register onException interceptor 144 | enableExceptionLogging : true, 145 | // Send messages to Sentry in a thread 146 | async : true, 147 | // Whether to include client cookies when sending request information to Sentry 148 | sendCookies : false, 149 | // Whether to include POST data (e.g. FORM) when sending request information to Sentry 150 | sendPostData : false, 151 | // Don't sent URL or FORM field values of these names to Sentry 152 | scrubFields : [ 'passwd', 'password', 'password_confirmation', 'secret', 'confirm_password', 'secret_token', 'APIToken', 'x-api-token', 'fwreinit' ], 153 | // Don't sent HTTP header values of these names to Sentry 154 | scrubHeaders : [ 'x-api-token', 'Authorization' ], 155 | // The current release of your app, used with Sentry release/deploy tracking. Ex. "myApp@2.3.0" 156 | release : '', 157 | // App environment, used to control notifications and filtering 158 | environment : 'production', 159 | // Client connection string for this project. Mutually exclusive with next 4 settings 160 | // Get this from the "settings" page on a project under "Client Keys (DSN)" 161 | DSN : '', 162 | // Sentry public client key for this project. (Not needed when using DSN) 163 | publicKey : '', 164 | // Sentry public client key for this project (Not needed when using DSN) 165 | privateKey : '', 166 | // Sentry projectID (Not needed when using DSN) 167 | projectID : 0, 168 | // URL of your Sentry server (Not needed when using DSN) 169 | sentryUrl : 'https://sentry.io', 170 | // posting to "#sentryUrl#/api/#projectID#/store" is deprecated, but backward compatible 171 | // set to "envelope" to send events to modern "#sentryUrl#/api/#projectID#/envelope" 172 | sentryEventEndpoint : "store", 173 | // name of your server 174 | serverName : cgi.server_name, 175 | // Absolute path to the root of your app, defaults to the webroot. 176 | // files in stacktrace frames contained under this path will be reported relative to it 177 | appRoot : expandPath('/'), 178 | // Default logger category. LogBox appender will pass through the LogBox category name 179 | logger : 'sentry', 180 | // Closure to return dynamic info of logged in user which will appear in Sentry error reports under "User". 181 | // No args are passed to the closure when used outside of a ColdBox app. 182 | userInfoUDF : function( event, rc, prc ){ 183 | return { 184 | // Standard user data Sentry looks for 185 | id : 123 186 | username : 'bwood', 187 | email : 'brad@bradwood.com', 188 | // Anything else you want 189 | cool : true, 190 | memberType : 'platinum' 191 | }; 192 | } 193 | } 194 | ``` 195 | 196 | ### Trace Data 197 | 198 | The Sentry service is configure to automatically pass along the [W3 standard `traceparent` header](https://www.w3.org/TR/trace-context/#traceparent-header), which [Sentry will ingest](https://develop.sentry.dev/sdk/telemetry/traces/#header-traceparent) and append to the entry as distributed tracing data. If the upstream does not send this header [the `cbotel` module](https://forgebox.io/view/cbotel) may be used to create the trace parent data. 199 | 200 | 201 | ### Interceptions 202 | 203 | The `onSentryEventCapture` interception point, allows other modules or framework listeners to contribute to sentry logs ( e.g providing tags or additional user info ) 204 | Example usage: 205 | 206 | ```java 207 | function onSentryEventCapture( event, rc, prc, interceptData ){ 208 | var sentryEvent = interceptData.event; 209 | sentryEvent.tags[ "foo" ] = "bar"; 210 | } 211 | ``` 212 | 213 | 214 | ### Credit 215 | 216 | This project is based on the fine open source work of others. 217 | 218 | * https://github.com/GiancarloGomez/sentry-cfml 219 | * https://github.com/jmacul2/raven-cfml 220 | 221 | ******************************************************************************** 222 | Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 223 | www.ortussolutions.com 224 | ******************************************************************************** 225 | 226 | #### HONOR GOES TO GOD ABOVE ALL 227 | 228 | Because of His grace, this project exists. If you don't like this, then don't read it, its not for you. 229 | 230 | > "Therefore being justified by faith, we have peace with God through our Lord Jesus Christ: 231 | By whom also we have access by faith into this grace wherein we stand, and rejoice in hope of the glory of God. 232 | And not only so, but we glory in tribulations also: knowing that tribulation worketh patience; 233 | And patience, experience; and experience, hope: 234 | And hope maketh not ashamed; because the love of God is shed abroad in our hearts by the 235 | Holy Ghost which is given unto us. ." Romans 5:5 236 | 237 | ### THE DAILY BREAD 238 | 239 | > "I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12 240 | -------------------------------------------------------------------------------- /server-adobe@2018.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Sentry-adobe@2018", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2018", 5 | "cfengine":"adobe@2018" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | }, 14 | "webroot": "test-harness" 15 | }, 16 | "openBrowser":"false", 17 | "cfconfig": { 18 | "file" : ".cfconfig.json" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server-adobe@2021.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Sentry-adobe@2021", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2021", 5 | "cfengine":"adobe@2021" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | }, 14 | "webroot": "test-harness" 15 | }, 16 | "jvm":{ 17 | "heapSize":"1024" 18 | }, 19 | "openBrowser":"false", 20 | "cfconfig": { 21 | "file" : ".cfconfig.json" 22 | }, 23 | "scripts" : { 24 | "onServerInstall":"cfpm install zip,debugger" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server-adobe@2023.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Sentry-adobe@2023", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2023", 5 | "cfengine":"adobe@2023" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | }, 14 | "webroot": "test-harness" 15 | }, 16 | "jvm":{ 17 | "heapSize":"1024" 18 | }, 19 | "openBrowser":"false", 20 | "cfconfig": { 21 | "file" : ".cfconfig.json" 22 | }, 23 | "scripts" : { 24 | "onServerInstall":"cfpm install zip,debugger" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server-boxlang@1.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "cfengine":"boxlang@1.0.0-snapshot", 4 | "serverHomeDirectory":".engine/boxlang" 5 | }, 6 | "name":"sentry-boxlang@1", 7 | "force":true, 8 | "openBrowser":false, 9 | "web":{ 10 | "directoryBrowsing":true, 11 | "http":{ 12 | "port":"60299" 13 | }, 14 | "rewrites":{ 15 | "enable":"true" 16 | }, 17 | "webroot":"test-harness", 18 | "aliases":{ 19 | "/moduleroot/sentry":"./" 20 | } 21 | }, 22 | "JVM":{ 23 | "javaVersion":"openjdk21_jdk_x64", 24 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9999 -Dboxlang.debugMode=true" 25 | }, 26 | "scripts":{ 27 | "onServerInitialInstall":"install bx-compat-cfml@be,bx-esapi --noSave" 28 | } 29 | } -------------------------------------------------------------------------------- /server-lucee@5.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Sentry-lucee@5", 3 | "app":{ 4 | "serverHomeDirectory":".engine/lucee5", 5 | "cfengine":"lucee@5" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | }, 14 | "webroot":"test-harness", 15 | "aliases":{ 16 | "/moduleroot/sentry":"../" 17 | } 18 | }, 19 | "openBrowser":"false", 20 | "cfconfig":{ 21 | "file":".cfconfig.json" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server-lucee@6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Sentry-lucee@6", 3 | "app":{ 4 | "serverHomeDirectory":".engine/lucee6", 5 | "cfengine":"lucee@6" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60299" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | }, 14 | "webroot": "test-harness" 15 | }, 16 | "openBrowser":"false", 17 | "cfconfig": { 18 | "file" : ".cfconfig.json" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | falseColdFusion2016 -------------------------------------------------------------------------------- /test-harness/.cflintrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test-harness/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | ******************************************************************************** 3 | Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | www.ortussolutions.com 5 | ******************************************************************************** 6 | */ 7 | component{ 8 | 9 | // UPDATE THE NAME OF THE MODULE IN TESTING BELOW 10 | request.MODULE_NAME = "sentry"; 11 | 12 | // Application properties 13 | this.name = hash( getCurrentTemplatePath() ); 14 | this.sessionManagement = true; 15 | this.sessionTimeout = createTimeSpan(0,0,15,0); 16 | this.setClientCookies = true; 17 | 18 | /************************************** 19 | LUCEE Specific Settings 20 | **************************************/ 21 | // buffer the output of a tag/function body to output in case of a exception 22 | this.bufferOutput = true; 23 | // Activate Gzip Compression 24 | this.compression = false; 25 | // Turn on/off white space managemetn 26 | this.whiteSpaceManagement = "smart"; 27 | // Turn on/off remote cfc content whitespace 28 | this.suppressRemoteComponentContent = false; 29 | 30 | // COLDBOX STATIC PROPERTY, DO NOT CHANGE UNLESS THIS IS NOT THE ROOT OF YOUR COLDBOX APP 31 | COLDBOX_APP_ROOT_PATH = getDirectoryFromPath( getCurrentTemplatePath() ); 32 | // The web server mapping to this application. Used for remote purposes or static purposes 33 | COLDBOX_APP_MAPPING = ""; 34 | // COLDBOX PROPERTIES 35 | COLDBOX_CONFIG_FILE = ""; 36 | // COLDBOX APPLICATION KEY OVERRIDE 37 | COLDBOX_APP_KEY = ""; 38 | 39 | // Mappings 40 | this.mappings[ "/root" ] = COLDBOX_APP_ROOT_PATH; 41 | 42 | // Map back to its root 43 | moduleRootPath = REReplaceNoCase( this.mappings[ "/root" ], "#request.MODULE_NAME#(\\|/)test-harness(\\|/)", "" ); 44 | modulePath = REReplaceNoCase( this.mappings[ "/root" ], "test-harness(\\|/)", "" ); 45 | 46 | // Module Root + Path Mappings 47 | this.mappings[ "/moduleroot" ] = moduleRootPath; 48 | this.mappings[ "/#request.MODULE_NAME#" ] = modulePath; 49 | 50 | // application start 51 | public boolean function onApplicationStart(){ 52 | application.cbBootstrap = new coldbox.system.Bootstrap( COLDBOX_CONFIG_FILE, COLDBOX_APP_ROOT_PATH, COLDBOX_APP_KEY, COLDBOX_APP_MAPPING ); 53 | application.cbBootstrap.loadColdbox(); 54 | return true; 55 | } 56 | 57 | // request start 58 | public boolean function onRequestStart(String targetPage){ 59 | 60 | // Process ColdBox Request 61 | application.cbBootstrap.onRequestStart( arguments.targetPage ); 62 | 63 | return true; 64 | } 65 | 66 | public void function onSessionStart(){ 67 | application.cbBootStrap.onSessionStart(); 68 | } 69 | 70 | public void function onSessionEnd( struct sessionScope, struct appScope ){ 71 | arguments.appScope.cbBootStrap.onSessionEnd( argumentCollection=arguments ); 72 | } 73 | 74 | public boolean function onMissingTemplate( template ){ 75 | return application.cbBootstrap.onMissingTemplate( argumentCollection=arguments ); 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /test-harness/box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Tester", 3 | "version":"0.0.0", 4 | "slug":"tester", 5 | "private":true, 6 | "description":"", 7 | "dependencies":{ 8 | "coldbox":"^7.0.0" 9 | }, 10 | "devDependencies":{ 11 | "testbox":"^3.0.0" 12 | }, 13 | "installPaths":{ 14 | "coldbox":"coldbox/", 15 | "testbox":"testbox/" 16 | }, 17 | "testbox":{ 18 | "runner":"http://localhost:60299/tests/runner.cfm" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test-harness/config/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a protection Application cfm for the config file. You do not 3 | * need to modify this file 4 | */ 5 | component{ 6 | abort; 7 | } -------------------------------------------------------------------------------- /test-harness/config/Coldbox.cfc: -------------------------------------------------------------------------------- 1 | component{ 2 | 3 | // Configure ColdBox Application 4 | function configure(){ 5 | 6 | // coldbox directives 7 | coldbox = { 8 | //Application Setup 9 | appName = "Module Tester", 10 | 11 | //Development Settings 12 | reinitPassword = "", 13 | handlersIndexAutoReload = true, 14 | modulesExternalLocation = [], 15 | 16 | //Implicit Events 17 | defaultEvent = "", 18 | requestStartHandler = "", 19 | requestEndHandler = "", 20 | applicationStartHandler = "", 21 | applicationEndHandler = "", 22 | sessionStartHandler = "", 23 | sessionEndHandler = "", 24 | missingTemplateHandler = "", 25 | 26 | //Error/Exception Handling 27 | exceptionHandler = "", 28 | onInvalidEvent = "", 29 | customErrorTemplate = "/coldbox/system/includes/BugReport.cfm", 30 | 31 | //Application Aspects 32 | handlerCaching = false, 33 | eventCaching = false 34 | }; 35 | 36 | // environment settings, create a detectEnvironment() method to detect it yourself. 37 | // create a function with the name of the environment so it can be executed if that environment is detected 38 | // the value of the environment is a list of regex patterns to match the cgi.http_host. 39 | environments = { 40 | development = "localhost,127\.0\.0\.1" 41 | }; 42 | 43 | // Module Directives 44 | modules = { 45 | // An array of modules names to load, empty means all of them 46 | include = [], 47 | // An array of modules names to NOT load, empty means none 48 | exclude = [] 49 | }; 50 | 51 | //Register interceptors as an array, we need order 52 | interceptors = [ 53 | ]; 54 | 55 | //LogBox DSL 56 | logBox = { 57 | // Define Appenders 58 | appenders = { 59 | files={class="coldbox.system.logging.appenders.RollingFileAppender", 60 | properties = { 61 | filename = "tester", filePath="/#appMapping#/logs", 62 | async = true 63 | } 64 | } 65 | }, 66 | // Root Logger 67 | root = { levelmax="DEBUG", appenders="*" }, 68 | // Implicit Level Categories 69 | info = [ "coldbox.system" ] 70 | }; 71 | 72 | moduleSettings = { 73 | sentry = { 74 | // Enable the Sentry LogBox Appender Bridge 75 | "enableLogBoxAppender" : true, 76 | // Min/Max levels for appender 77 | "levelMin" : "FATAL", 78 | "levelMax" : "ERROR", 79 | // Enable/disable error logging 80 | "enableExceptionLogging" = true, 81 | //"publicKey" : getSystemSetting( "SENTRY_PUBLICKEY", "" ), 82 | //"privateKey" : getSystemSetting( "SENTRY_PRIVATEKEY", "" ), 83 | //"projectID" : getSystemSetting( "SENTRY_PROJECTID", "" ), 84 | //"sentryUrl" : getSystemSetting( "SENTRY_URL", "" ), 85 | DSN : getSystemSetting( "SENTRY_DSN", "" ), 86 | async : false, 87 | userInfoUDF = function(){ 88 | return { 89 | username : 'woodsb', 90 | email : 'brad@bradwood.com', 91 | cool : true 92 | }; 93 | } 94 | } 95 | }; 96 | 97 | } 98 | 99 | /** 100 | * Load the Module you are testing 101 | */ 102 | function afterAspectsLoad( event, interceptData, rc, prc ){ 103 | controller.getModuleService() 104 | .registerAndActivateModule( 105 | moduleName = request.MODULE_NAME, 106 | invocationPath = "moduleroot" 107 | ); 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /test-harness/config/Routes.cfm: -------------------------------------------------------------------------------- 1 |  2 | // Allow unique URL or combination of URLs, we recommend both enabled 3 | setUniqueURLS(false); 4 | // Auto reload configuration, true in dev makes sense to reload the routes on every request 5 | //setAutoReload(false); 6 | // Sets automatic route extension detection and places the extension in the rc.format variable 7 | // setExtensionDetection(true); 8 | // The valid extensions this interceptor will detect 9 | // setValidExtensions('xml,json,jsont,rss,html,htm'); 10 | // If enabled, the interceptor will throw a 406 exception that an invalid format was detected or just ignore it 11 | // setThrowOnInvalidExtension(true); 12 | 13 | // Base URL 14 | if( len(getSetting('AppMapping') ) lte 1){ 15 | setBaseURL("http://#cgi.HTTP_HOST#/"); 16 | } 17 | else{ 18 | setBaseURL("http://#cgi.HTTP_HOST#/#getSetting('AppMapping')#/"); 19 | } 20 | 21 | // Your Application Routes 22 | addRoute(pattern=":handler/:action?"); 23 | 24 | 25 | /** Developers can modify the CGI.PATH_INFO value in advance of the SES 26 | interceptor to do all sorts of manipulations in advance of route 27 | detection. If provided, this function will be called by the SES 28 | interceptor instead of referencing the value CGI.PATH_INFO. 29 | 30 | This is a great place to perform custom manipulations to fix systemic 31 | URL issues your Web site may have or simplify routes for i18n sites. 32 | 33 | @Event The ColdBox RequestContext Object 34 | **/ 35 | function PathInfoProvider(Event){ 36 | /* Example: 37 | var URI = CGI.PATH_INFO; 38 | if (URI eq "api/foo/bar") 39 | { 40 | Event.setProxyRequest(true); 41 | return "some/other/value/for/your/routes"; 42 | } 43 | */ 44 | return CGI.PATH_INFO; 45 | } 46 | -------------------------------------------------------------------------------- /test-harness/config/WireBox.cfc: -------------------------------------------------------------------------------- 1 | component extends="coldbox.system.ioc.config.Binder"{ 2 | 3 | /** 4 | * Configure WireBox, that's it! 5 | */ 6 | function configure(){ 7 | 8 | // The WireBox configuration structure DSL 9 | wireBox = { 10 | // Scope registration, automatically register a wirebox injector instance on any CF scope 11 | // By default it registeres itself on application scope 12 | scopeRegistration = { 13 | enabled = true, 14 | scope = "application", // server, cluster, session, application 15 | key = "wireBox" 16 | }, 17 | 18 | // DSL Namespace registrations 19 | customDSL = { 20 | // namespace = "mapping name" 21 | }, 22 | 23 | // Custom Storage Scopes 24 | customScopes = { 25 | // annotationName = "mapping name" 26 | }, 27 | 28 | // Package scan locations 29 | scanLocations = [], 30 | 31 | // Stop Recursions 32 | stopRecursions = [], 33 | 34 | // Parent Injector to assign to the configured injector, this must be an object reference 35 | parentInjector = "", 36 | 37 | // Register all event listeners here, they are created in the specified order 38 | listeners = [ 39 | // { class="", name="", properties={} } 40 | ] 41 | }; 42 | 43 | // Map Bindings below 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /test-harness/handlers/Main.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My Event Handler Hint 3 | */ 4 | component extends="coldbox.system.EventHandler"{ 5 | 6 | /** 7 | * Executes before all handler actions 8 | */ 9 | any function preHandler( event, rc, prc, action, eventArguments ){ 10 | log.error( "Sending some more info to Sentry" ); 11 | } 12 | 13 | /** 14 | * Index 15 | */ 16 | any function index( event, rc, prc ){ 17 | throw( message="Thrown from main.cfc in index method", type="ThrownFromMain" ); 18 | } 19 | 20 | // Run on first init 21 | any function onAppInit( event, rc, prc ){ 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /test-harness/index.cfm: -------------------------------------------------------------------------------- 1 |  2 | 9 | 10 | -------------------------------------------------------------------------------- /test-harness/layouts/Main.cfm: -------------------------------------------------------------------------------- 1 |  2 |

Module Tester

3 |
4 | #renderView()# 5 |
6 |
-------------------------------------------------------------------------------- /test-harness/tests/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | ******************************************************************************** 3 | Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | www.ortussolutions.com 5 | ******************************************************************************** 6 | */ 7 | component { 8 | 9 | request.MODULE_NAME = "sentry"; 10 | 11 | // APPLICATION CFC PROPERTIES 12 | this.name = "ColdBoxTestingSuite" & hash( getCurrentTemplatePath() ); 13 | this.sessionManagement = true; 14 | this.sessionTimeout = createTimespan( 0, 0, 15, 0 ); 15 | this.applicationTimeout = createTimespan( 0, 0, 15, 0 ); 16 | this.setClientCookies = true; 17 | 18 | // Create testing mapping 19 | this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 20 | 21 | // The application root 22 | rootPath = reReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); 23 | this.mappings[ "/root" ] = rootPath; 24 | 25 | // UPDATE THE NAME OF THE MODULE IN TESTING BELOW 26 | request.MODULE_NAME = "sentry"; 27 | 28 | // The module root path 29 | moduleRootPath = reReplaceNoCase( 30 | this.mappings[ "/root" ], 31 | "#request.module_name#(\\|/)test-harness(\\|/)", 32 | "" 33 | ); 34 | this.mappings[ "/moduleroot" ] = moduleRootPath; 35 | this.mappings[ "/#request.MODULE_NAME#" ] = moduleRootPath & "#request.MODULE_NAME#"; 36 | 37 | 38 | /** 39 | * Fires on every test request. It builds a Virtual ColdBox application for you 40 | * 41 | * @targetPage The requested page 42 | */ 43 | public boolean function onRequestStart( targetPage ){ 44 | // Set a high timeout for long running tests 45 | setting requestTimeout ="9999"; 46 | // New ColdBox Virtual Application Starter 47 | request.coldBoxVirtualApp= new coldbox.system.testing.VirtualApp(); 48 | 49 | // If hitting the runner or specs, prep our virtual app 50 | if ( getBaseTemplatePath().replace( expandPath( "/tests" ), "" ).reFindNoCase( "(runner|specs)" ) ) { 51 | request.coldBoxVirtualApp.startup(); 52 | } 53 | 54 | // Reload for fresh results 55 | if ( structKeyExists( url, "fwreinit" ) ) { 56 | if ( structKeyExists( server, "lucee" ) ) { 57 | pagePoolClear(); 58 | } 59 | // ormReload(); 60 | request.coldBoxVirtualApp.restart(); 61 | } 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * Fires when the testing requests end and the ColdBox application is shutdown 68 | */ 69 | public void function onRequestEnd( required targetPage ){ 70 | request.coldBoxVirtualApp.shutdown(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /test-harness/tests/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | // No cf debugging 3 | cfsetting( showdebugoutput="false" ); 4 | // Path Navigation 5 | param name="url.path" default=""; 6 | // Root Tests Directory 7 | rootMapping = "/tests/specs"; 8 | rootPath = expandPath( rootMapping ); 9 | targetPath = rootPath; 10 | // Append navigation path 11 | if( len( url.path ) ){ 12 | targetPath = getCanonicalPath( rootpath & "/" & url.path ); 13 | // Avoid traversals 14 | if( !findNoCase( rootpath, targetPath ) ){ 15 | targetPath = rootpath; 16 | } 17 | } 18 | // Get the actual execution path 19 | executePath = rootMapping & ( len( url.path ) ? "/#url.path#" : "/" ); 20 | // Directory Runner 21 | if( !isNull( url.action ) ){ 22 | if( directoryExists( targetPath ) ){ 23 | writeOutput( "#new testbox.system.TestBox( directory=executePath ).run()#" ); 24 | } else { 25 | writeOutput( "

Invalid Directory: #encodeForHTML( targetPath )#

" ); 26 | } 27 | abort; 28 | } 29 | // Get target path listing 30 | qResults = directoryList( targetPath, false, "query", "", "name" ); 31 | // Get the back path 32 | if( len( url.path ) ){ 33 | backPath = replacenocase( url.path, listLast( url.path, "/" ), "" ); 34 | backPath = reReplace( backpath, "/$", "" ); 35 | } 36 | // TestBox Assets 37 | ASSETS_DIR = expandPath( "/testbox/system/reports/assets" ); 38 | TESTBOX_VERSION = new testBox.system.TestBox().getVersion(); 39 |
40 | 41 | 42 | 43 | 44 | 45 | TestBox Browser 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | 60 |
61 |
62 | 63 |
64 | v#TESTBOX_VERSION# 65 |
66 | 70 | 75 | 76 |
77 |
78 | 79 | 80 |
81 |
82 |
83 | 84 |

TestBox Test Browser:

85 |

86 | Below is a listing of the files and folders starting from your root #rootMapping#. You can click on individual tests in order to execute them 87 | or click on the Run All button on your left and it will execute a directory runner from the visible folder. 88 |

89 | 90 |
91 | #targetPath.replace( rootPath, "" )# 92 | 93 | 94 | 95 | 96 | 97 | 98 |
99 |
100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 113 | &##x271A; #qResults.name# 114 | 115 |
116 | 117 | 122 | #qResults.name# 123 | 124 |
125 | 126 | 131 | #qResults.name# 132 | 133 |
134 | 135 | #qResults.name# 136 |
137 |
138 | 139 |
140 |
141 |
142 |
143 |
144 |
145 | 146 | 147 | 148 |
149 | -------------------------------------------------------------------------------- /test-harness/tests/runner.cfm: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test-harness/tests/specs/SentryTests.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My BDD Test 3 | */ 4 | component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { 5 | 6 | this.loadColdbox = true; 7 | 8 | /*********************************** LIFE CYCLE Methods ***********************************/ 9 | 10 | // executes before all suites+specs in the run() method 11 | function beforeAll(){ 12 | super.beforeAll(); 13 | } 14 | 15 | // executes after all suites+specs in the run() method 16 | function afterAll(){ 17 | super.afterAll(); 18 | } 19 | 20 | /*********************************** BDD SUITES ***********************************/ 21 | 22 | function run(){ 23 | // all your suites go here. 24 | describe( "Sentry Module", function(){ 25 | beforeEach( function( currentSpec ){ 26 | setup(); 27 | } ); 28 | 29 | it( "should register library", function(){ 30 | var service = getSentry(); 31 | expect( service ).toBeComponent(); 32 | } ); 33 | 34 | it( "can log message", function(){ 35 | var service = getSentry(); 36 | service.captureMessage( "This is a test message" ); 37 | } ); 38 | 39 | it( "can log via LogBox", function(){ 40 | getLogbox().getRootLogger().error( "Custom Boom", { "extra" : "info" } ); 41 | } ); 42 | 43 | it( "can log Java exception", function(){ 44 | var getNull = function(){ 45 | }; 46 | try { 47 | foo = createObject( "java", "java.io.File" ).init( getNull() ); 48 | } catch ( any e ) { 49 | getLogbox().getRootLogger().error( e.message, e ); 50 | } 51 | } ); 52 | 53 | it( "can log exception with no tagContext", function(){ 54 | try { 55 | throw( "Missing tag Context" ); 56 | } catch ( any e ) { 57 | var newE = {}; 58 | for ( var key in e ) { 59 | if ( key != "TagContext" ) { 60 | newE[ key ] = e[ key ]; 61 | } 62 | } 63 | getLogbox().getRootLogger().error( "Missing tag Context", newE ); 64 | } 65 | } ); 66 | 67 | it( "can log exception with Extra Error Info", function(){ 68 | try { 69 | throw( "Extra Error Info" ); 70 | } catch ( any e ) { 71 | e.NativeErrorCode = "This is my NativeErrorCode"; 72 | e.SQLState = "This is my SQLState"; 73 | e.Sql = "This is my Sql"; 74 | e.queryError = "This is my queryError"; 75 | e.where = "This is my where"; 76 | e.ErrNumber = "This is my ErrNumber"; 77 | e.MissingFileName = "This is my MissingFileName"; 78 | e.LockName = "This is my LockName"; 79 | e.LockOperation = "This is my LockOperation"; 80 | e.ErrorCode = "This is my ErrorCode"; 81 | e.ExtendedInfo = "This is my ExtendedInfo"; 82 | 83 | getLogbox().getRootLogger().error( "Extra Error Info", e ); 84 | } 85 | } ); 86 | 87 | it( "should trap exceptions and do logging", function(){ 88 | expect( function(){ 89 | execute( "main.index" ); 90 | } ).toThrow( "ThrownFromMain" ); 91 | } ); 92 | 93 | it( "can log a message with extra info automatically added", function(){ 94 | var service = prepareMock( getSentry() ); 95 | service.setEnabled( true ); 96 | service.$( "post" ); 97 | 98 | service.addExtraInfoUdf( "queries", function(){ 99 | return [ "foo", "bar" ]; 100 | } ); 101 | service.addExtraInfoUdf( "qb", function(){ 102 | return [ "foo", "bar" ]; 103 | } ); 104 | service.captureMessage( "This is a test message" ); 105 | var extra = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ).extra; 106 | expect( extra ).toHaveKey( "queries" ); 107 | expect( extra ).toHaveKey( "qb" ); 108 | expect( extra.queries ).toBe( [ "foo", "bar" ] ); 109 | expect( extra.qb ).toBe( [ "foo", "bar" ] ); 110 | } ); 111 | 112 | it( "Can capture traceparent data from the http request", function(){ 113 | var service = prepareMock( getSentry() ); 114 | var testTraceParent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"; 115 | service.setEnabled( true ); 116 | service.$( 117 | method = "getHTTPDataForRequest", 118 | callback = function(){ 119 | return { 120 | "headers" : { "traceparent" : testTraceParent }, 121 | "content" : "" 122 | }; 123 | } 124 | ); 125 | service.$( "post" ); 126 | 127 | service.captureMessage( "This is a test message" ); 128 | var traceParent = service.$callLog( "post" ).post[ 1 ][ 5 ]; 129 | expect( traceParent ).toBe( testTraceParent ); 130 | } ); 131 | 132 | 133 | 134 | it( "Can capture traceparent data from the cbotel module", function(){ 135 | var service = prepareMock( getSentry() ); 136 | var testTraceParent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"; 137 | service.setEnabled( true ); 138 | service.setColdbox( getController() ); 139 | 140 | getController() 141 | .getRequestService() 142 | .getContext() 143 | .setPrivateValue( "openTelemetry", { "traceparent" : testTraceParent } ); 144 | service.$( "post" ); 145 | service.captureMessage( "This is a test message" ); 146 | var traceParent = service.$callLog( "post" ).post[ 1 ][ 5 ]; 147 | expect( traceParent ).toBe( testTraceParent ); 148 | } ); 149 | } ); 150 | } 151 | 152 | 153 | 154 | private function getSentry(){ 155 | return getWireBox().getInstance( "SentryService@sentry" ); 156 | } 157 | 158 | } 159 | --------------------------------------------------------------------------------