From 95b78c269f5727b6a076aca84171be75c044b2d8 Mon Sep 17 00:00:00 2001 From: jclausen Date: Wed, 25 Dec 2024 18:22:37 +0000 Subject: [PATCH 01/27] Apply cfformat changes --- models/SentryService.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index cf47fe7..2845275 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -647,7 +647,7 @@ component accessors=true singleton { var header = ""; var timeVars = getTimeVars(); var httpRequestData = getHTTPDataForRequest(); - var traceParent = httpRequestData.headers.traceParent ?: ""; + var traceParent = httpRequestData.headers.traceParent ?: ""; // Add global metadata arguments.captureStruct[ "event_id" ] = lCase( replace( createUUID(), "-", "", "all" ) ); From e0fc9b7beeca3b5b978789ca36ffb3a84c0bb9ec Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 25 Dec 2024 18:57:40 +0000 Subject: [PATCH 02/27] Version bump --- box.json | 2 +- changelog.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/box.json b/box.json index ccd679e..e415806 100644 --- a/box.json +++ b/box.json @@ -1,7 +1,7 @@ { "name":"Sentry", "author":"Ortus Solutions ", - "version":"2.1.5", + "version":"2.1.6", "slug":"sentry", "type":"modules", "homepage":"https://github.com/coldbox-modules/sentry", diff --git a/changelog.md b/changelog.md index 8627281..f2daf70 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.1.5] - 2024-12-25 + ### Fixed - [Issue #37](https://github.com/coldbox-modules/sentry/issues/37) - Fix var-scoping inside conditional ## [2.1.4] - 2024-12-24 @@ -49,12 +51,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create first module version -[Unreleased]: https://github.com/coldbox-modules/sentry/compare/v2.1.4...HEAD +[Unreleased]: https://github.com/coldbox-modules/sentry/compare/v2.1.5...HEAD -[2.1.4]: https://github.com/coldbox-modules/sentry/compare/HEAD...v2.1.4 +[2.1.5]: https://github.com/coldbox-modules/sentry/compare/v2.1.4...v2.1.5 +[2.1.4]: https://github.com/coldbox-modules/sentry/compare/HEAD...v2.1.4 [2.1.3]: https://github.com/coldbox-modules/sentry/compare/v2.1.0...v2.1.3 - [2.1.0]: https://github.com/coldbox-modules/sentry/compare/57864cae5969ad38eee194db5a6b2798e91967b3...v2.1.0 From a2471dbee8f6a1b6041f67112834cb7086f37e4d Mon Sep 17 00:00:00 2001 From: Oscar Tisnado <68830577+otisnado@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:16:27 -0600 Subject: [PATCH 03/27] ci: add boxlang to matrix and enable workflow dispatch for triggering boxlang tests --- .github/workflows/snapshot.yml | 1 + .github/workflows/tests.yml | 8 ++++++++ server-boxlang@1.json | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 server-boxlang@1.json diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 98cef2e..0b553df 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -4,6 +4,7 @@ on: push: branches: - 'development' + workflow_dispatch: # Unique group name per workflow-branch/tag combo concurrency: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bbb8ed5..71e7cec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,6 +35,9 @@ jobs: - coldboxVersion: "be" cfengine: "adobe@2023" experimental: true + - coldboxVersion: "be" + cfengine: "boxlang@1" + experimental: true steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -54,6 +57,11 @@ jobs: - name: Setup CommandBox CLI uses: Ortus-Solutions/setup-commandbox@v2.0.1 + - name: Update Commandbox Boxlang Module + if: ${{ matrix.cfengine == 'boxlang@1' }} + run: + box install --force commandbox-boxlang + # Not Needed in this module #- name: Setup Environment For Testing Process # run: | diff --git a/server-boxlang@1.json b/server-boxlang@1.json new file mode 100644 index 0000000..65cb3a6 --- /dev/null +++ b/server-boxlang@1.json @@ -0,0 +1,29 @@ +{ + "app":{ + "cfengine":"boxlang@1.0.0-snapshot", + "serverHomeDirectory":".engine/boxlang" + }, + "name":"sentry-boxlang@1", + "force":true, + "openBrowser":false, + "web":{ + "directoryBrowsing":true, + "http":{ + "port":"60299" + }, + "rewrites":{ + "enable":"true" + }, + "webroot":"test-harness", + "aliases":{ + "/moduleroot/sentry":"./" + } + }, + "JVM":{ + "javaVersion":"openjdk21_jdk_x64", + "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9999 -Dboxlang.debugMode=true" + }, + "scripts":{ + "onServerInitialInstall":"install bx-compat-cfml@be,bx-esapi --noSave" + } +} \ No newline at end of file From 480d1bfafee9c154275a79d24273f4d8f7f3df76 Mon Sep 17 00:00:00 2001 From: Oscar Tisnado <68830577+otisnado@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:56:36 -0600 Subject: [PATCH 04/27] ci: use ubuntu-24.04 runner --- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/snapshot.yml | 2 +- .github/workflows/tests.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a4ed296..9508e5d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,7 +20,7 @@ jobs: # Format PR format_check: name: Checks Source Code Formatting - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17062c7..9327cb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: ########################################################################################## build: name: Build & Publish - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -133,7 +133,7 @@ jobs: prep_next_release: name: Prep Next Release if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [ build ] steps: # Checkout development diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 0b553df..7b76e82 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -24,7 +24,7 @@ jobs: ########################################################################################## format: name: Code Auto-Formatting - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 71e7cec..6ec41c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ on: jobs: tests: name: Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: DB_USER: root DB_PASSWORD: root From 732df85f6e218bb8f7bf5ca564b90a797a36a04a Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Mon, 10 Nov 2025 18:15:46 -0500 Subject: [PATCH 05/27] add default settings for `showJavaStackTrace` and `throwOnPostError` to module config --- ModuleConfig.cfc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index fb8a389..acd9317 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -73,7 +73,9 @@ component { "platform" : "cfml", "logger" : ( !isNull( controller ) ? controller.getSetting( "appName" ) : "sentry" ), "userInfoUDF" : "", - "extraInfoUDFs" : {} + "extraInfoUDFs" : {}, // these two settings may be added but they + "showJavaStackTrace" : false, + "throwOnPostError" : false }; // Try to look up the release based on a box.json From 9e63252628076352ccf24f2642ce7edc811ae3f2 Mon Sep 17 00:00:00 2001 From: jclausen <5255645+jclausen@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:18:33 +0000 Subject: [PATCH 06/27] Apply cfformat changes --- ModuleConfig.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index acd9317..8e32d48 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -73,7 +73,7 @@ component { "platform" : "cfml", "logger" : ( !isNull( controller ) ? controller.getSetting( "appName" ) : "sentry" ), "userInfoUDF" : "", - "extraInfoUDFs" : {}, // these two settings may be added but they + "extraInfoUDFs" : {}, // these two settings may be added but they "showJavaStackTrace" : false, "throwOnPostError" : false }; From 1e6d42d408e34fc38bcc38a7dbbccab6033569ab Mon Sep 17 00:00:00 2001 From: Michael Offner Date: Tue, 30 Jun 2026 11:55:38 +0200 Subject: [PATCH 07/27] handle null value --- models/SentryService.cfc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 2845275..a950da4 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -718,7 +718,9 @@ component accessors=true singleton { if ( listFindNoCase( "id,email,ip_address,username", key ) ) { key = lCase( key ); } - correctCasingUserInfo[ key ] = thisUserInfo[ key ]; + if(!isNull(thisUserInfo[ key ])) { + correctCasingUserInfo[ key ] = thisUserInfo[ key ]; + } } arguments.captureStruct[ "user" ] = correctCasingUserInfo; From ada391e770a036286fd34908c4ad26c4602f62ac Mon Sep 17 00:00:00 2001 From: bdw429s <584009+bdw429s@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:10:28 +0000 Subject: [PATCH 08/27] Apply cfformat changes --- models/SentryService.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index a950da4..0ce6f4f 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -718,7 +718,7 @@ component accessors=true singleton { if ( listFindNoCase( "id,email,ip_address,username", key ) ) { key = lCase( key ); } - if(!isNull(thisUserInfo[ key ])) { + if ( !isNull( thisUserInfo[ key ] ) ) { correctCasingUserInfo[ key ] = thisUserInfo[ key ]; } } From 05346d9800edde0c3a6b3f9acaae54bf29be9748 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:36:51 -0700 Subject: [PATCH 09/27] feat: send Java stack traces as structured exception values in Sentry events When showJavaStackTrace=true, the Java stack trace is now parsed into structured Sentry exception values with proper frames (function, filename, lineno, in_app) instead of being dumped as a raw text blob in 'extra'. This gives Sentry proper structured frames for the Java side of the call stack, enabling: - Frame-by-frame navigation in the Sentry UI - Proper exception chaining via 'Caused by:' parsing - Correct grouping and deduplication - in_app heuristics for application vs framework frames The parseJavaStackTrace() method handles: - Standard 'at package.Class.method(File.java:line)' frames - 'Native Method' and 'Unknown Source' frames (no line number) - 'Caused by:' nested exception chains - '... N more' truncated frame indicators Also includes: - Null-safety fix for structifyObject() (from inleague-api patch) - Deprecation of removeTabsOnJavaStackTrace parameter - 7 new test cases covering parsing, in_app detection, Caused by chains, empty TagContext fallback, and edge cases --- models/SentryService.cfc | 205 ++++++++++++++++++++-- test-harness/tests/specs/SentryTests.cfc | 214 +++++++++++++++++++++++ 2 files changed, 404 insertions(+), 15 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 0ce6f4f..0b14fa0 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -344,8 +344,8 @@ component accessors=true singleton { * @level The level to log * @path The path to the script currently executing * @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 - * @showJavaStackTrace Passes Java Stack Trace as a string to the extra attribute - * @removeTabsOnJavaStackTrace Removes the tab on the child lines in the Stack Trace + * @showJavaStackTrace When true, parses the Java stack trace and sends it as structured exception entries in exception.values with proper Sentry frames. + * @removeTabsOnJavaStackTrace Deprecated — no longer needed. Kept for backward compatibility. * @additionalData Additional metadata to store with the event - passed into the extra attribute * @cgiVars Parameters to send to Sentry, defaults to the CGI Scope * @useThread Option to send post to Sentry in its own thread @@ -422,16 +422,8 @@ component accessors=true singleton { sentryException.message = arguments.message & " " & sentryException.message; } - if ( arguments.showJavaStackTrace ) { - st = reReplace( - arguments.exception.StackTrace, - "\r", - "", - "All" - ); - if ( arguments.removeTabsOnJavaStackTrace ) st = reReplace( st, "\t", "", "All" ); - sentryExceptionExtra[ "Java StackTrace" ] = listToArray( st, chr( 10 ) ); - } + // Java stack trace is now sent as a structured exception entry in exception.values + // via parseJavaStackTrace() below, not as a raw text blob in extra. if ( !isNull( arguments.additionalData ) ) { sentryExceptionExtra[ "Additional Data" ] = arguments.additionalData; @@ -507,10 +499,17 @@ component accessors=true singleton { "type" : arguments.exception.type & " Error", "stacktrace" : { "frames" : [] } }; - sentryException[ "exception" ] = { "values" : [ currentException ] }; - + // If showJavaStackTrace is enabled, parse the Java stack trace and add it + // as a second (or more) entry in exception.values. This gives Sentry proper + // structured frames for the Java side instead of a raw text blob in "extra". + if ( arguments.showJavaStackTrace && len( arguments.exception.StackTrace ) ) { + var javaExceptions = parseJavaStackTrace( arguments.exception.StackTrace ); + for ( var je in javaExceptions ) { + arrayAppend( sentryException[ "exception" ].values, je ); + } + } /* * STACKTRACE INTERFACE @@ -601,6 +600,180 @@ component accessors=true singleton { ); } + /** + * Parse a raw Java stack trace string into Sentry exception values. + * + * Handles the standard Java stack trace format including: + * - Exception class name and message on the first line(s) + * - "at package.Class.method(File.java:line)" frames + * - "at package.Class.method(Native Method)" frames + * - "... N more" truncated frame indicators + * - "Caused by:" nested exception chains + * + * Returns an array of Sentry exception value structs, each with: + * - type: The Java exception class name + * - value: The exception message + * - stacktrace: { frames: [...] } with parsed frame objects + */ + private array function parseJavaStackTrace( required string stackTrace ){ + var result = []; + var lines = listToArray( arguments.stackTrace, chr( 10 ) ); + var curType = ""; + var curValue = ""; + var curFrames = []; + var inException = false; + + // Regex for "at com.example.Class.method(File.java:42)" + // Note: (.+) for class is GREEDY to match the full qualified name up to the last dot before ( + var atPattern = "^\\s*at\\s+(.+)\\.(.+?)\\((.+?):(\\d+)\\)$"; + // Regex for "at com.example.Class.method(Native Method)" or "(Unknown Source)" + var atNoLinePat = "^\\s*at\\s+(.+)\\.(.+?)\\((.+?)\\)$"; + // Regex for "Caused by: java.lang.Exception: message" (message is optional) + var causedByPat = "^\\s*Caused by:\\s+(.+?)(?:\\s*:\\s*(.*))?$"; + // Regex for initial exception line "java.lang.Exception: message" + var exceptionPat = "^(.+?):\\s*(.*)$"; + // Regex for "... N more" lines + var morePat = "^\\s*\\.\\.\\.\\s+\\d+\\s+more\\s*$"; + + for ( var line in lines ) { + // Skip blank lines + if ( !len( trim( line ) ) ) { + continue; + } + + // "... N more" — skip, these are duplicated frames + if ( reFind( morePat, line ) ) { + continue; + } + + // "Caused by: ..." — save current exception, start a new one + var causedByMatch = reFind( causedByPat, line, 1, true ); + if ( causedByMatch.len[ 1 ] ) { + // Flush previous exception + if ( len( curType ) ) { + arrayAppend( + result, + _buildJavaExceptionValue( curType, curValue, curFrames ) + ); + } + curType = trim( mid( line, causedByMatch.pos[ 2 ], causedByMatch.len[ 2 ] ) ); + curValue = trim( mid( line, causedByMatch.pos[ 3 ], causedByMatch.len[ 3 ] ) ); + curFrames = []; + inException = true; + continue; + } + + // "at ..." frame line + var atMatch = reFind( atPattern, line, 1, true ); + if ( atMatch.len[ 1 ] ) { + inException = true; + var atClass = mid( line, atMatch.pos[ 2 ], atMatch.len[ 2 ] ); + var atMethod = mid( line, atMatch.pos[ 3 ], atMatch.len[ 3 ] ); + var atFile = mid( line, atMatch.pos[ 4 ], atMatch.len[ 4 ] ); + var atLine = val( mid( line, atMatch.pos[ 5 ], atMatch.len[ 5 ] ) ); + + arrayAppend( + curFrames, + _buildJavaFrame( atClass, atMethod, atFile, atLine ) + ); + continue; + } + + // "at ..." frame without line number (Native Method, Unknown Source) + var atNoLineMatch = reFind( atNoLinePat, line, 1, true ); + if ( atNoLineMatch.len[ 1 ] ) { + inException = true; + var atClass2 = mid( line, atNoLineMatch.pos[ 2 ], atNoLineMatch.len[ 2 ] ); + var atMethod2 = mid( line, atNoLineMatch.pos[ 3 ], atNoLineMatch.len[ 3 ] ); + + arrayAppend( + curFrames, + _buildJavaFrame( atClass2, atMethod2, "", 0 ) + ); + continue; + } + + // If we haven't hit any "at" lines yet, this is part of the exception header + if ( !inException ) { + var exMatch = reFind( exceptionPat, line, 1, true ); + if ( exMatch.len[ 1 ] ) { + curType = trim( mid( line, exMatch.pos[ 2 ], exMatch.len[ 2 ] ) ); + curValue = trim( mid( line, exMatch.pos[ 3 ], exMatch.len[ 3 ] ) ); + } else if ( !len( curType ) ) { + // First line might just be the exception class + curType = trim( line ); + } else { + // Continuation of the message + curValue &= " " & trim( line ); + } + } + } + + // Flush the last exception + if ( len( curType ) ) { + arrayAppend( + result, + _buildJavaExceptionValue( curType, curValue, curFrames ) + ); + } + + return result; + } + + /** + * Build a single Sentry exception value struct for a Java exception. + */ + private struct function _buildJavaExceptionValue( + required string type, + required string value, + required array frames + ){ + return { + "type" : arguments.type, + "value" : arguments.value, + "stacktrace" : { "frames" : arguments.frames } + }; + } + + /** + * Build a single Sentry stacktrace frame from a Java "at" line. + * + * @className Fully qualified class name (e.g. "com.example.MyClass") + * @method Method name (e.g. "myMethod") + * @fileName Source file name (e.g. "MyClass.java"), may be empty + * @lineNumber Line number, 0 if unknown + */ + private struct function _buildJavaFrame( + required string className, + required string method, + required string fileName, + required numeric lineNumber + ){ + var frame = { + "function" : arguments.className & "." & arguments.method, + "filename" : arguments.fileName, + "lineno" : arguments.lineNumber, + "abs_path" : arguments.className, + "in_app" : false, + "context_line" : "", + "pre_context" : [], + "post_context" : [] + }; + + // Heuristic: if the class doesn't start with common framework prefixes, + // it's probably application code + if ( + !reFindNoCase( + "^(java\\.|javax\\.|sun\\.|com\\.sun\\.|org\\.apache\\.|org\\.springframework\\.|org\\.hibernate\\.|lucee\\.|boxlang\\.)", + arguments.className + ) + ) { + frame[ "in_app" ] = true; + } + + return frame; + } + // recursivley replace any CFC instances with structs function structifyObject( o, name = "" ){ var result = {}; @@ -611,7 +784,9 @@ component accessors=true singleton { return structReduce( o, function( acc, k, v ){ - if ( !isCustomFunction( v ) ) { + if ( isNull( arguments.v ) ) { + acc[ k ] = javacast( "null", 0 ); + } else if ( !isCustomFunction( v ) ) { if ( isObject( v ) ) { acc[ k ] = structifyObject( v, getMetadata( v ).name ); } else if ( isStruct( v ) ) { diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index d78a660..c5974ed 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -146,6 +146,220 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { var traceParent = service.$callLog( "post" ).post[ 1 ][ 5 ]; expect( traceParent ).toBe( testTraceParent ); } ); + + // ========== Java Stack Trace Parsing Tests ========== + + it( "can parse a simple Java stack trace into exception values", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Something failed", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.NullPointerException: null object reference + at com.example.MyClass.myMethod(MyClass.java:42) + at com.example.MyClass.otherMethod(MyClass.java:100) + at org.apache.catalina.core.StandardWrapper.invoke(StandardWrapper.java:500)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + // Should have 2 entries: BoxLang exception + Java exception + expect( excValues.len() ).toBe( 2 ); + + // First entry is the BoxLang CFML exception + expect( excValues[ 1 ].type ).toBe( "application Error" ); + expect( excValues[ 1 ].stacktrace.frames.len() ).toBe( 0 ); + + // Second entry is the parsed Java exception + expect( excValues[ 2 ].type ).toBe( "java.lang.NullPointerException" ); + expect( excValues[ 2 ].value ).toBe( "null object reference" ); + expect( excValues[ 2 ].stacktrace.frames.len() ).toBe( 3 ); + + // Verify first frame parsing + var frame1 = excValues[ 2 ].stacktrace.frames[ 1 ]; + expect( frame1.function ).toBe( "com.example.MyClass.myMethod" ); + expect( frame1.filename ).toBe( "MyClass.java" ); + expect( frame1.lineno ).toBe( 42 ); + expect( frame1.abs_path ).toBe( "com.example.MyClass" ); + } ); + + it( "marks application frames as in_app and framework frames as not", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Error", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.io.IOException: file not found + at com.example.service.FileHelper.read(FileHelper.java:55) + at org.apache.commons.io.IOUtils.toString(IOUtils.java:2000) + at java.io.FileInputStream.(FileInputStream.java:138)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + var frames = excValues[ 2 ].stacktrace.frames; + + // com.example = in_app + expect( frames[ 1 ].in_app ).toBe( true ); + // org.apache.commons = not in_app + expect( frames[ 2 ].in_app ).toBe( false ); + // java.io = not in_app + expect( frames[ 3 ].in_app ).toBe( false ); + } ); + + it( "parses Native Method frames without line numbers", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Error", + "detail" : "", + "type" : "expression", + "TagContext" : [], + "StackTrace" : "java.lang.NullPointerException + at java.io.FileInputStream.open0(Native Method) + at java.io.FileInputStream.open(FileInputStream.java:195) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var frames = payload.exception.values[ 2 ].stacktrace.frames; + + // Native Method frame — no line number + expect( frames[ 1 ].lineno ).toBe( 0 ); + expect( frames[ 1 ].function ).toBe( "java.io.FileInputStream.open0" ); + expect( frames[ 1 ].filename ).toBe( "" ); + + // Regular frame with line number + expect( frames[ 2 ].lineno ).toBe( 195 ); + expect( frames[ 2 ].filename ).toBe( "FileInputStream.java" ); + } ); + + it( "parses Caused by chains into multiple exception values", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Wrapper error", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.RuntimeException: something went wrong + at com.example.App.main(App.java:10) + Caused by: java.io.FileNotFoundException: /tmp/missing.txt + at java.io.FileInputStream.open0(Native Method) + at java.io.FileInputStream.(FileInputStream.java:138) + ... 3 more" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + // Should have 3 entries: BoxLang + RuntimeException + FileNotFoundException + expect( excValues.len() ).toBe( 3 ); + + // Second entry: RuntimeException + expect( excValues[ 2 ].type ).toBe( "java.lang.RuntimeException" ); + expect( excValues[ 2 ].value ).toBe( "something went wrong" ); + expect( excValues[ 2 ].stacktrace.frames.len() ).toBe( 1 ); + + // Third entry: FileNotFoundException + expect( excValues[ 3 ].type ).toBe( "java.io.FileNotFoundException" ); + expect( excValues[ 3 ].value ).toBe( "/tmp/missing.txt" ); + // "... 3 more" should be skipped, only 2 actual frames + expect( excValues[ 3 ].stacktrace.frames.len() ).toBe( 2 ); + } ); + + it( "does not add Java stack trace entries when showJavaStackTrace is false", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Error", + "detail" : "", + "type" : "application", + "TagContext" : [ { "TEMPLATE" : "/test.cfm", "LINE" : 1 } ], + "StackTrace" : "java.lang.RuntimeException: boom + at com.example.App.main(App.java:10)" + }; + + service.captureException( exception = testException, showJavaStackTrace = false ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + // Should only have 1 entry: BoxLang exception + expect( excValues.len() ).toBe( 1 ); + expect( excValues[ 1 ].type ).toBe( "application Error" ); + } ); + + it( "forces showJavaStackTrace when TagContext is empty", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Error", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.NullPointerException + at com.example.App.run(App.java:25)" + }; + + // Even with showJavaStackTrace defaulting to false, + // empty TagContext should force it on + service.captureException( exception = testException ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + expect( excValues.len() ).toBe( 2 ); + expect( excValues[ 2 ].type ).toBe( "java.lang.NullPointerException" ); + } ); + + it( "handles exception messages with no colon separator", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Error", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "NullPointerException + at com.example.App.run(App.java:25)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + expect( excValues.len() ).toBe( 2 ); + expect( excValues[ 2 ].type ).toBe( "NullPointerException" ); + } ); } ); } From a9d0ccec200de00f9f06a6eacac7c7ca48bd3639 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:39:33 -0700 Subject: [PATCH 10/27] fix: address review findings for Java stack trace parsing - Add \r stripping before splitting by \n (regression from old code) - Add jakarta. to in_app exclusion list (Jakarta EE classes) - Add Suppressed: exception handling (Java 7+ suppressed exceptions) - These fix medium-priority issues from code review --- models/SentryService.cfc | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 0b14fa0..eafb4a4 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -617,7 +617,9 @@ component accessors=true singleton { */ private array function parseJavaStackTrace( required string stackTrace ){ var result = []; - var lines = listToArray( arguments.stackTrace, chr( 10 ) ); + // Strip \r to handle Windows-style line endings consistently + var cleaned = reReplace( arguments.stackTrace, "\\r", "", "All" ); + var lines = listToArray( cleaned, chr( 10 ) ); var curType = ""; var curValue = ""; var curFrames = []; @@ -630,6 +632,8 @@ component accessors=true singleton { var atNoLinePat = "^\\s*at\\s+(.+)\\.(.+?)\\((.+?)\\)$"; // Regex for "Caused by: java.lang.Exception: message" (message is optional) var causedByPat = "^\\s*Caused by:\\s+(.+?)(?:\\s*:\\s*(.*))?$"; + // Regex for "Suppressed: java.lang.Exception: message" (Java 7+) + var suppressedPat = "^\\s*Suppressed:\\s+(.+?)(?:\\s*:\\s*(.*))?$"; // Regex for initial exception line "java.lang.Exception: message" var exceptionPat = "^(.+?):\\s*(.*)$"; // Regex for "... N more" lines @@ -663,6 +667,23 @@ component accessors=true singleton { continue; } + // "Suppressed: ..." — same handling as Caused by (Java 7+) + var suppressedMatch = reFind( suppressedPat, line, 1, true ); + if ( suppressedMatch.len[ 1 ] ) { + // Flush previous exception + if ( len( curType ) ) { + arrayAppend( + result, + _buildJavaExceptionValue( curType, curValue, curFrames ) + ); + } + curType = trim( mid( line, suppressedMatch.pos[ 2 ], suppressedMatch.len[ 2 ] ) ); + curValue = trim( mid( line, suppressedMatch.pos[ 3 ], suppressedMatch.len[ 3 ] ) ); + curFrames = []; + inException = true; + continue; + } + // "at ..." frame line var atMatch = reFind( atPattern, line, 1, true ); if ( atMatch.len[ 1 ] ) { @@ -764,7 +785,7 @@ component accessors=true singleton { // it's probably application code if ( !reFindNoCase( - "^(java\\.|javax\\.|sun\\.|com\\.sun\\.|org\\.apache\\.|org\\.springframework\\.|org\\.hibernate\\.|lucee\\.|boxlang\\.)", + "^(java\\.|javax\\.|jakarta\\.|sun\\.|com\\.sun\\.|org\\.apache\\.|org\\.springframework\\.|org\\.hibernate\\.|lucee\\.|boxlang\\.)", arguments.className ) ) { From 57274951a94804c0c2f9aed8ade433ac6d356ba3 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:40:29 -0700 Subject: [PATCH 11/27] test: add Suppressed exception parsing test --- test-harness/tests/specs/SentryTests.cfc | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index c5974ed..1b102f1 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -360,6 +360,38 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { expect( excValues.len() ).toBe( 2 ); expect( excValues[ 2 ].type ).toBe( "NullPointerException" ); } ); + + it( "parses Suppressed exceptions into separate values", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Error", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.io.IOException: original error + at com.example.App.main(App.java:10) + Suppressed: java.io.IOException: suppressed error + at com.example.App.helper(App.java:20) + ... 1 more" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + // BoxLang + original IOException + suppressed IOException + expect( excValues.len() ).toBe( 3 ); + expect( excValues[ 2 ].type ).toBe( "java.io.IOException" ); + expect( excValues[ 2 ].value ).toBe( "original error" ); + expect( excValues[ 2 ].stacktrace.frames.len() ).toBe( 1 ); + expect( excValues[ 3 ].type ).toBe( "java.io.IOException" ); + expect( excValues[ 3 ].value ).toBe( "suppressed error" ); + expect( excValues[ 3 ].stacktrace.frames.len() ).toBe( 1 ); + } ); } ); } From 3ca293d09c89482bab8659a54262557b842e7904 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:31:04 -0700 Subject: [PATCH 12/27] fix: rewrite Java stack trace parser to use string operations instead of regex BoxLang's regex engine doesn't handle non-greedy quantifiers and complex patterns reliably. Rewrote parseJavaStackTrace() to use find(), mid(), left() and simple string operations for parsing. This fixes all 5 test failures: - Exception type was including the message (regex not splitting on colon) - Frames array was empty (at-pattern regex not matching) - Caused by chains only producing 2 exceptions instead of 3 - Suppressed exceptions not being parsed Also fixes the upstream test failures caused by BoxLang incompatibility with the old regex-based approach. --- models/SentryService.cfc | 132 +++++++++++++++----------- test-harness/server-sentry-tests.json | 11 +++ 2 files changed, 90 insertions(+), 53 deletions(-) create mode 100644 test-harness/server-sentry-tests.json diff --git a/models/SentryService.cfc b/models/SentryService.cfc index eafb4a4..4aafb11 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -625,34 +625,21 @@ component accessors=true singleton { var curFrames = []; var inException = false; - // Regex for "at com.example.Class.method(File.java:42)" - // Note: (.+) for class is GREEDY to match the full qualified name up to the last dot before ( - var atPattern = "^\\s*at\\s+(.+)\\.(.+?)\\((.+?):(\\d+)\\)$"; - // Regex for "at com.example.Class.method(Native Method)" or "(Unknown Source)" - var atNoLinePat = "^\\s*at\\s+(.+)\\.(.+?)\\((.+?)\\)$"; - // Regex for "Caused by: java.lang.Exception: message" (message is optional) - var causedByPat = "^\\s*Caused by:\\s+(.+?)(?:\\s*:\\s*(.*))?$"; - // Regex for "Suppressed: java.lang.Exception: message" (Java 7+) - var suppressedPat = "^\\s*Suppressed:\\s+(.+?)(?:\\s*:\\s*(.*))?$"; - // Regex for initial exception line "java.lang.Exception: message" - var exceptionPat = "^(.+?):\\s*(.*)$"; - // Regex for "... N more" lines - var morePat = "^\\s*\\.\\.\\.\\s+\\d+\\s+more\\s*$"; - for ( var line in lines ) { // Skip blank lines if ( !len( trim( line ) ) ) { continue; } + var trimmedLine = trim( line ); + // "... N more" — skip, these are duplicated frames - if ( reFind( morePat, line ) ) { + if ( left( trimmedLine, 3 ) == "..." && reFind( "^\\.\\.\\.\\s+\\d+\\s+more", trimmedLine ) ) { continue; } // "Caused by: ..." — save current exception, start a new one - var causedByMatch = reFind( causedByPat, line, 1, true ); - if ( causedByMatch.len[ 1 ] ) { + if ( left( trimmedLine, 9 ) == "Caused by" ) { // Flush previous exception if ( len( curType ) ) { arrayAppend( @@ -660,16 +647,23 @@ component accessors=true singleton { _buildJavaExceptionValue( curType, curValue, curFrames ) ); } - curType = trim( mid( line, causedByMatch.pos[ 2 ], causedByMatch.len[ 2 ] ) ); - curValue = trim( mid( line, causedByMatch.pos[ 3 ], causedByMatch.len[ 3 ] ) ); + // Parse: "Caused by: java.lang.Exception: message" + var afterPrefix = trim( mid( trimmedLine, 11 ) ); // skip "Caused by:" + var colonPos = find( ":", afterPrefix ); + if ( colonPos > 1 ) { + curType = trim( mid( afterPrefix, 1, colonPos - 1 ) ); + curValue = trim( mid( afterPrefix, colonPos + 1 ) ); + } else { + curType = afterPrefix; + curValue = ""; + } curFrames = []; inException = true; continue; } // "Suppressed: ..." — same handling as Caused by (Java 7+) - var suppressedMatch = reFind( suppressedPat, line, 1, true ); - if ( suppressedMatch.len[ 1 ] ) { + if ( left( trimmedLine, 10 ) == "Suppressed" ) { // Flush previous exception if ( len( curType ) ) { arrayAppend( @@ -677,55 +671,87 @@ component accessors=true singleton { _buildJavaExceptionValue( curType, curValue, curFrames ) ); } - curType = trim( mid( line, suppressedMatch.pos[ 2 ], suppressedMatch.len[ 2 ] ) ); - curValue = trim( mid( line, suppressedMatch.pos[ 3 ], suppressedMatch.len[ 3 ] ) ); + // Parse: "Suppressed: java.lang.Exception: message" + var afterSuppressed = trim( mid( trimmedLine, 12 ) ); // skip "Suppressed:" + var colonPos2 = find( ":", afterSuppressed ); + if ( colonPos2 > 1 ) { + curType = trim( mid( afterSuppressed, 1, colonPos2 - 1 ) ); + curValue = trim( mid( afterSuppressed, colonPos2 + 1 ) ); + } else { + curType = afterSuppressed; + curValue = ""; + } curFrames = []; inException = true; continue; } // "at ..." frame line - var atMatch = reFind( atPattern, line, 1, true ); - if ( atMatch.len[ 1 ] ) { + if ( left( trimmedLine, 3 ) == "at " ) { inException = true; - var atClass = mid( line, atMatch.pos[ 2 ], atMatch.len[ 2 ] ); - var atMethod = mid( line, atMatch.pos[ 3 ], atMatch.len[ 3 ] ); - var atFile = mid( line, atMatch.pos[ 4 ], atMatch.len[ 4 ] ); - var atLine = val( mid( line, atMatch.pos[ 5 ], atMatch.len[ 5 ] ) ); - - arrayAppend( - curFrames, - _buildJavaFrame( atClass, atMethod, atFile, atLine ) - ); - continue; - } - - // "at ..." frame without line number (Native Method, Unknown Source) - var atNoLineMatch = reFind( atNoLinePat, line, 1, true ); - if ( atNoLineMatch.len[ 1 ] ) { - inException = true; - var atClass2 = mid( line, atNoLineMatch.pos[ 2 ], atNoLineMatch.len[ 2 ] ); - var atMethod2 = mid( line, atNoLineMatch.pos[ 3 ], atNoLineMatch.len[ 3 ] ); + // Parse: "at com.example.Class.method(File.java:42)" + // or: "at com.example.Class.method(Native Method)" + var afterAt = mid( trimmedLine, 4 ); // skip "at " + var openParen = find( "(", afterAt ); + var closeParen = find( ")", afterAt ); + + if ( openParen > 1 && closeParen > openParen ) { + var qualifiedName = mid( afterAt, 1, openParen - 1 ); + var parenContent = mid( afterAt, openParen + 1, closeParen - openParen - 1 ); + + // Split qualified name on last dot: "com.example.Class.method" → class + method + var lastDot = 0; + for ( var p = len( qualifiedName ); p >= 1; p-- ) { + if ( mid( qualifiedName, p, 1 ) == "." ) { + lastDot = p; + break; + } + } - arrayAppend( - curFrames, - _buildJavaFrame( atClass2, atMethod2, "", 0 ) - ); + if ( lastDot > 1 ) { + var atClass = mid( qualifiedName, 1, lastDot - 1 ); + var atMethod = mid( qualifiedName, lastDot + 1 ); + + // Parse paren content: "File.java:42" or "Native Method" + var colonInParen = find( ":", parenContent ); + if ( colonInParen > 1 ) { + var atFile = mid( parenContent, 1, colonInParen - 1 ); + var atLine = val( mid( parenContent, colonInParen + 1 ) ); + arrayAppend( + curFrames, + _buildJavaFrame( atClass, atMethod, atFile, atLine ) + ); + } else { + // Native Method, Unknown Source, etc. + arrayAppend( + curFrames, + _buildJavaFrame( atClass, atMethod, "", 0 ) + ); + } + } + } continue; } // If we haven't hit any "at" lines yet, this is part of the exception header if ( !inException ) { - var exMatch = reFind( exceptionPat, line, 1, true ); - if ( exMatch.len[ 1 ] ) { - curType = trim( mid( line, exMatch.pos[ 2 ], exMatch.len[ 2 ] ) ); - curValue = trim( mid( line, exMatch.pos[ 3 ], exMatch.len[ 3 ] ) ); + var colonPos3 = find( ":", trimmedLine ); + if ( colonPos3 > 1 ) { + // Check if it looks like an exception class name (no spaces before colon) + var beforeColon = mid( trimmedLine, 1, colonPos3 - 1 ); + if ( !find( " ", beforeColon ) ) { + curType = beforeColon; + curValue = trim( mid( trimmedLine, colonPos3 + 1 ) ); + } else { + // Space before colon — probably a continuation of the message + curValue = curValue & " " & trimmedLine; + } } else if ( !len( curType ) ) { // First line might just be the exception class - curType = trim( line ); + curType = trimmedLine; } else { // Continuation of the message - curValue &= " " & trim( line ); + curValue = curValue & " " & trimmedLine; } } } diff --git a/test-harness/server-sentry-tests.json b/test-harness/server-sentry-tests.json new file mode 100644 index 0000000..82cb9d2 --- /dev/null +++ b/test-harness/server-sentry-tests.json @@ -0,0 +1,11 @@ +{ + "app":{ + "cfengine":"boxlang" + }, + "name":"sentry-tests", + "web":{ + "http":{ + "port":"8599" + } + } +} From b999857af56268c88cd9e5b3938f12f72cecc99c Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:32:56 -0700 Subject: [PATCH 13/27] fix: replace regex-based in_app heuristic with string prefix matching BoxLang's reFindNoCase doesn't handle complex regex patterns reliably. Replaced with simple left() prefix checks against a list of framework packages. More readable and guaranteed to work across engines. --- models/SentryService.cfc | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 4aafb11..ad3d34c 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -809,12 +809,19 @@ component accessors=true singleton { // Heuristic: if the class doesn't start with common framework prefixes, // it's probably application code - if ( - !reFindNoCase( - "^(java\\.|javax\\.|jakarta\\.|sun\\.|com\\.sun\\.|org\\.apache\\.|org\\.springframework\\.|org\\.hibernate\\.|lucee\\.|boxlang\\.)", - arguments.className - ) - ) { + var frameworkPrefixes = [ + "java.", "javax.", "jakarta.", "sun.", "com.sun.", + "org.apache.", "org.springframework.", "org.hibernate.", + "lucee.", "boxlang." + ]; + var isFramework = false; + for ( var prefix in frameworkPrefixes ) { + if ( left( arguments.className, len( prefix ) ) == prefix ) { + isFramework = true; + break; + } + } + if ( !isFramework ) { frame[ "in_app" ] = true; } From 527fa8dce2c6d6c123b2124f4d90cad2ac22c5f0 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:42:58 -0700 Subject: [PATCH 14/27] fix: make upstream tests cross-engine compatible 1. 'can log Java exception': Use null-safe operator for e.message which is null when catching Java exceptions on both BoxLang and Lucee 2. 'can log exception with no tagContext': Replace 'for (var key in e)' iteration with duplicate()+structDelete() which works on all engines. BoxLang exceptions don't support key iteration like Lucee/ACF. --- test-harness/tests/specs/SentryTests.cfc | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index 1b102f1..31adf36 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -46,7 +46,7 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { try { foo = createObject( "java", "java.io.File" ).init( getNull() ); } catch ( any e ) { - getLogbox().getRootLogger().error( e.message, e ); + getLogbox().getRootLogger().error( e.message ?: "Java exception", e ); } } ); @@ -54,12 +54,8 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { try { throw( "Missing tag Context" ); } catch ( any e ) { - var newE = {}; - for ( var key in e ) { - if ( key != "TagContext" ) { - newE[ key ] = e[ key ]; - } - } + var newE = duplicate( e ); + structDelete( newE, "TagContext" ); getLogbox().getRootLogger().error( "Missing tag Context", newE ); } } ); From 48973bfc7fa2f62b81fa5b00e796c9fa11c9af75 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:03:42 -0700 Subject: [PATCH 15/27] style: address review nits from PR #1 1. Make prefix checks include the colon: 'Caused by:' and 'Suppressed:' instead of 'Caused by' and 'Suppressed' 2. Simplify last-dot iteration with listToArray/arrayToList instead of manual character loop 3. Fix indentation after removing the if(lastDot>1) block --- models/SentryService.cfc | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index ad3d34c..f106a79 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -639,7 +639,7 @@ component accessors=true singleton { } // "Caused by: ..." — save current exception, start a new one - if ( left( trimmedLine, 9 ) == "Caused by" ) { + if ( left( trimmedLine, 10 ) == "Caused by:" ) { // Flush previous exception if ( len( curType ) ) { arrayAppend( @@ -663,7 +663,7 @@ component accessors=true singleton { } // "Suppressed: ..." — same handling as Caused by (Java 7+) - if ( left( trimmedLine, 10 ) == "Suppressed" ) { + if ( left( trimmedLine, 11 ) == "Suppressed:" ) { // Flush previous exception if ( len( curType ) ) { arrayAppend( @@ -700,17 +700,10 @@ component accessors=true singleton { var parenContent = mid( afterAt, openParen + 1, closeParen - openParen - 1 ); // Split qualified name on last dot: "com.example.Class.method" → class + method - var lastDot = 0; - for ( var p = len( qualifiedName ); p >= 1; p-- ) { - if ( mid( qualifiedName, p, 1 ) == "." ) { - lastDot = p; - break; - } - } - - if ( lastDot > 1 ) { - var atClass = mid( qualifiedName, 1, lastDot - 1 ); - var atMethod = mid( qualifiedName, lastDot + 1 ); + var parts = listToArray( qualifiedName, "." ); + var atMethod = parts[ parts.len() ]; + parts.deleteAt( parts.len() ); + var atClass = arrayToList( parts, "." ); // Parse paren content: "File.java:42" or "Native Method" var colonInParen = find( ":", parenContent ); @@ -729,9 +722,8 @@ component accessors=true singleton { ); } } - } - continue; - } + continue; + } // If we haven't hit any "at" lines yet, this is part of the exception header if ( !inException ) { From 6302404a839ef47b199ec60b4397a4dadefcf381 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:15:50 -0700 Subject: [PATCH 16/27] style: apply cfformat formatting to modified files --- models/SentryService.cfc | 100 +++++++++++------------ test-harness/tests/specs/SentryTests.cfc | 4 +- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index f106a79..1a0ade7 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -344,7 +344,7 @@ component accessors=true singleton { * @level The level to log * @path The path to the script currently executing * @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 - * @showJavaStackTrace When true, parses the Java stack trace and sends it as structured exception entries in exception.values with proper Sentry frames. + * @showJavaStackTrace When true, parses the Java stack trace and sends it as structured exception entries in exception.values with proper Sentry frames. * @removeTabsOnJavaStackTrace Deprecated — no longer needed. Kept for backward compatibility. * @additionalData Additional metadata to store with the event - passed into the extra attribute * @cgiVars Parameters to send to Sentry, defaults to the CGI Scope @@ -616,13 +616,13 @@ component accessors=true singleton { * - stacktrace: { frames: [...] } with parsed frame objects */ private array function parseJavaStackTrace( required string stackTrace ){ - var result = []; + var result = []; // Strip \r to handle Windows-style line endings consistently - var cleaned = reReplace( arguments.stackTrace, "\\r", "", "All" ); - var lines = listToArray( cleaned, chr( 10 ) ); - var curType = ""; - var curValue = ""; - var curFrames = []; + var cleaned = reReplace( arguments.stackTrace, "\\r", "", "All" ); + var lines = listToArray( cleaned, chr( 10 ) ); + var curType = ""; + var curValue = ""; + var curFrames = []; var inException = false; for ( var line in lines ) { @@ -642,14 +642,11 @@ component accessors=true singleton { if ( left( trimmedLine, 10 ) == "Caused by:" ) { // Flush previous exception if ( len( curType ) ) { - arrayAppend( - result, - _buildJavaExceptionValue( curType, curValue, curFrames ) - ); + arrayAppend( result, _buildJavaExceptionValue( curType, curValue, curFrames ) ); } // Parse: "Caused by: java.lang.Exception: message" var afterPrefix = trim( mid( trimmedLine, 11 ) ); // skip "Caused by:" - var colonPos = find( ":", afterPrefix ); + var colonPos = find( ":", afterPrefix ); if ( colonPos > 1 ) { curType = trim( mid( afterPrefix, 1, colonPos - 1 ) ); curValue = trim( mid( afterPrefix, colonPos + 1 ) ); @@ -657,7 +654,7 @@ component accessors=true singleton { curType = afterPrefix; curValue = ""; } - curFrames = []; + curFrames = []; inException = true; continue; } @@ -666,14 +663,11 @@ component accessors=true singleton { if ( left( trimmedLine, 11 ) == "Suppressed:" ) { // Flush previous exception if ( len( curType ) ) { - arrayAppend( - result, - _buildJavaExceptionValue( curType, curValue, curFrames ) - ); + arrayAppend( result, _buildJavaExceptionValue( curType, curValue, curFrames ) ); } // Parse: "Suppressed: java.lang.Exception: message" var afterSuppressed = trim( mid( trimmedLine, 12 ) ); // skip "Suppressed:" - var colonPos2 = find( ":", afterSuppressed ); + var colonPos2 = find( ":", afterSuppressed ); if ( colonPos2 > 1 ) { curType = trim( mid( afterSuppressed, 1, colonPos2 - 1 ) ); curValue = trim( mid( afterSuppressed, colonPos2 + 1 ) ); @@ -681,49 +675,47 @@ component accessors=true singleton { curType = afterSuppressed; curValue = ""; } - curFrames = []; + curFrames = []; inException = true; continue; } // "at ..." frame line if ( left( trimmedLine, 3 ) == "at " ) { - inException = true; + inException = true; // Parse: "at com.example.Class.method(File.java:42)" // or: "at com.example.Class.method(Native Method)" - var afterAt = mid( trimmedLine, 4 ); // skip "at " - var openParen = find( "(", afterAt ); + var afterAt = mid( trimmedLine, 4 ); // skip "at " + var openParen = find( "(", afterAt ); var closeParen = find( ")", afterAt ); if ( openParen > 1 && closeParen > openParen ) { var qualifiedName = mid( afterAt, 1, openParen - 1 ); - var parenContent = mid( afterAt, openParen + 1, closeParen - openParen - 1 ); + var parenContent = mid( + afterAt, + openParen + 1, + closeParen - openParen - 1 + ); // Split qualified name on last dot: "com.example.Class.method" → class + method - var parts = listToArray( qualifiedName, "." ); + var parts = listToArray( qualifiedName, "." ); var atMethod = parts[ parts.len() ]; parts.deleteAt( parts.len() ); var atClass = arrayToList( parts, "." ); - // Parse paren content: "File.java:42" or "Native Method" - var colonInParen = find( ":", parenContent ); - if ( colonInParen > 1 ) { - var atFile = mid( parenContent, 1, colonInParen - 1 ); - var atLine = val( mid( parenContent, colonInParen + 1 ) ); - arrayAppend( - curFrames, - _buildJavaFrame( atClass, atMethod, atFile, atLine ) - ); - } else { - // Native Method, Unknown Source, etc. - arrayAppend( - curFrames, - _buildJavaFrame( atClass, atMethod, "", 0 ) - ); - } - } - continue; + // Parse paren content: "File.java:42" or "Native Method" + var colonInParen = find( ":", parenContent ); + if ( colonInParen > 1 ) { + var atFile = mid( parenContent, 1, colonInParen - 1 ); + var atLine = val( mid( parenContent, colonInParen + 1 ) ); + arrayAppend( curFrames, _buildJavaFrame( atClass, atMethod, atFile, atLine ) ); + } else { + // Native Method, Unknown Source, etc. + arrayAppend( curFrames, _buildJavaFrame( atClass, atMethod, "", 0 ) ); } + } + continue; + } // If we haven't hit any "at" lines yet, this is part of the exception header if ( !inException ) { @@ -750,10 +742,7 @@ component accessors=true singleton { // Flush the last exception if ( len( curType ) ) { - arrayAppend( - result, - _buildJavaExceptionValue( curType, curValue, curFrames ) - ); + arrayAppend( result, _buildJavaExceptionValue( curType, curValue, curFrames ) ); } return result; @@ -802,9 +791,16 @@ component accessors=true singleton { // Heuristic: if the class doesn't start with common framework prefixes, // it's probably application code var frameworkPrefixes = [ - "java.", "javax.", "jakarta.", "sun.", "com.sun.", - "org.apache.", "org.springframework.", "org.hibernate.", - "lucee.", "boxlang." + "java.", + "javax.", + "jakarta.", + "sun.", + "com.sun.", + "org.apache.", + "org.springframework.", + "org.hibernate.", + "lucee.", + "boxlang." ]; var isFramework = false; for ( var prefix in frameworkPrefixes ) { @@ -830,9 +826,9 @@ component accessors=true singleton { return structReduce( o, function( acc, k, v ){ - if ( isNull( arguments.v ) ) { - acc[ k ] = javacast( "null", 0 ); - } else if ( !isCustomFunction( v ) ) { + if ( isNull( arguments.v ) ) { + acc[ k ] = javacast( "null", 0 ); + } else if ( !isCustomFunction( v ) ) { if ( isObject( v ) ) { acc[ k ] = structifyObject( v, getMetadata( v ).name ); } else if ( isStruct( v ) ) { diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index 31adf36..a98a6a8 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -163,8 +163,8 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { service.captureException( exception = testException, showJavaStackTrace = true ); - var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); - var excValues = payload.exception.values; + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; // Should have 2 entries: BoxLang exception + Java exception expect( excValues.len() ).toBe( 2 ); From 978e8c0d25ed780dac2ba9bd4225e7426971bee0 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:20:59 -0700 Subject: [PATCH 17/27] chore: remove local test server config that was accidentally committed --- test-harness/server-sentry-tests.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 test-harness/server-sentry-tests.json diff --git a/test-harness/server-sentry-tests.json b/test-harness/server-sentry-tests.json deleted file mode 100644 index 82cb9d2..0000000 --- a/test-harness/server-sentry-tests.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "app":{ - "cfengine":"boxlang" - }, - "name":"sentry-tests", - "web":{ - "http":{ - "port":"8599" - } - } -} From 0993ab3a750a1d8db70591c8d6308b613059128d Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:21:43 -0700 Subject: [PATCH 18/27] refactor: address Jon's review on upstream PR #45 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Gate Java stack trace parsing on TagContext being empty — no need for the overhead when CFML frames are already available 2. Extract duplicated 'Caused by:'/'Suppressed:' parsing into _parseExceptionPrefix() helper function 3. Add test verifying Java traces are skipped when TagContext is present 4. Add .gitignore entries for local server test configs --- .gitignore | 5 +- models/SentryService.cfc | 71 +++++++++++------------- test-harness/tests/specs/SentryTests.cfc | 25 +++++++++ 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 750db63..df56d36 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ logs/** # don't put token test-harness/config/token.cfm test-harness/.env -/modules/ \ No newline at end of file +/modules/ +# Local server configs (test artifacts) +server-sentry-*.json +test-harness/server-sentry-*.json diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 1a0ade7..07b6d7c 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -501,10 +501,11 @@ component accessors=true singleton { }; sentryException[ "exception" ] = { "values" : [ currentException ] }; - // If showJavaStackTrace is enabled, parse the Java stack trace and add it - // as a second (or more) entry in exception.values. This gives Sentry proper - // structured frames for the Java side instead of a raw text blob in "extra". - if ( arguments.showJavaStackTrace && len( arguments.exception.StackTrace ) ) { + // If showJavaStackTrace is enabled AND there's no tagContext, parse the + // Java stack trace and add it as a second (or more) entry in exception.values. + // When tagContext is available, the CFML frames are sufficient — no need + // for the overhead of parsing the raw Java stack trace. + if ( arguments.showJavaStackTrace && !tagContext.len() && len( arguments.exception.StackTrace ) ) { var javaExceptions = parseJavaStackTrace( arguments.exception.StackTrace ); for ( var je in javaExceptions ) { arrayAppend( sentryException[ "exception" ].values, je ); @@ -638,45 +639,18 @@ component accessors=true singleton { continue; } - // "Caused by: ..." — save current exception, start a new one - if ( left( trimmedLine, 10 ) == "Caused by:" ) { + // "Caused by: ..." or "Suppressed: ..." — save current exception, start a new one + if ( left( trimmedLine, 10 ) == "Caused by:" || left( trimmedLine, 11 ) == "Suppressed:" ) { // Flush previous exception if ( len( curType ) ) { arrayAppend( result, _buildJavaExceptionValue( curType, curValue, curFrames ) ); } - // Parse: "Caused by: java.lang.Exception: message" - var afterPrefix = trim( mid( trimmedLine, 11 ) ); // skip "Caused by:" - var colonPos = find( ":", afterPrefix ); - if ( colonPos > 1 ) { - curType = trim( mid( afterPrefix, 1, colonPos - 1 ) ); - curValue = trim( mid( afterPrefix, colonPos + 1 ) ); - } else { - curType = afterPrefix; - curValue = ""; - } - curFrames = []; - inException = true; - continue; - } - - // "Suppressed: ..." — same handling as Caused by (Java 7+) - if ( left( trimmedLine, 11 ) == "Suppressed:" ) { - // Flush previous exception - if ( len( curType ) ) { - arrayAppend( result, _buildJavaExceptionValue( curType, curValue, curFrames ) ); - } - // Parse: "Suppressed: java.lang.Exception: message" - var afterSuppressed = trim( mid( trimmedLine, 12 ) ); // skip "Suppressed:" - var colonPos2 = find( ":", afterSuppressed ); - if ( colonPos2 > 1 ) { - curType = trim( mid( afterSuppressed, 1, colonPos2 - 1 ) ); - curValue = trim( mid( afterSuppressed, colonPos2 + 1 ) ); - } else { - curType = afterSuppressed; - curValue = ""; - } - curFrames = []; - inException = true; + var prefixLen = ( left( trimmedLine, 10 ) == "Caused by:" ) ? 10 : 11; + var parsed = _parseExceptionPrefix( trimmedLine, prefixLen ); + curType = parsed.type; + curValue = parsed.value; + curFrames = []; + inException = true; continue; } @@ -748,6 +722,25 @@ component accessors=true singleton { return result; } + /** + * Parse a "Caused by:" or "Suppressed:" exception prefix line. + * Returns { type, value }. + */ + private struct function _parseExceptionPrefix( + required string line, + required numeric prefixLen + ){ + var afterPrefix = trim( mid( arguments.line, arguments.prefixLen + 1 ) ); + var colonPos = find( ":", afterPrefix ); + if ( colonPos > 1 ) { + return { + "type" : trim( mid( afterPrefix, 1, colonPos - 1 ) ), + "value" : trim( mid( afterPrefix, colonPos + 1 ) ) + }; + } + return { "type" : afterPrefix, "value" : "" }; + } + /** * Build a single Sentry exception value struct for a Java exception. */ diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index a98a6a8..f1caf10 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -388,6 +388,31 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { expect( excValues[ 3 ].value ).toBe( "suppressed error" ); expect( excValues[ 3 ].stacktrace.frames.len() ).toBe( 1 ); } ); + + it( "skips Java stack trace parsing when TagContext is available", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Error", + "detail" : "", + "type" : "application", + "TagContext" : [ { "TEMPLATE" : "/test.cfm", "LINE" : 1 } ], + "StackTrace" : "java.lang.RuntimeException: boom + at com.example.App.main(App.java:10)" + }; + + // Even with showJavaStackTrace=true, TagContext is available + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + // Should only have 1 entry — CFML frames are sufficient + expect( excValues.len() ).toBe( 1 ); + expect( excValues[ 1 ].type ).toBe( "application Error" ); + } ); } ); } From 39a7ebd5d9132e2da4d0192e1c050029a6067e06 Mon Sep 17 00:00:00 2001 From: "Karlin [bot]" <3655094+karlin[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:33:25 -0700 Subject: [PATCH 19/27] fix: Adobe CF compatibility for mid() and exception handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Adobe CF requires 3 parameters for mid() — the 2-parameter form (mid(str, start)) is not supported. Added length parameter to all mid() calls in parseJavaStackTrace() and _parseExceptionPrefix(). 2. Fix 'can log exception with no tagContext' test — duplicate(e) fails on Adobe CF because exception objects are Java-backed. Build the struct manually with known keys instead. Both fixes are engine-agnostic and pass on BoxLang 1.15, Lucee 5, and Adobe CF 2021. --- models/SentryService.cfc | 14 +++++++++----- test-harness/tests/specs/SentryTests.cfc | 10 ++++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 07b6d7c..839b0d0 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -659,7 +659,7 @@ component accessors=true singleton { inException = true; // Parse: "at com.example.Class.method(File.java:42)" // or: "at com.example.Class.method(Native Method)" - var afterAt = mid( trimmedLine, 4 ); // skip "at " + var afterAt = ( len( trimmedLine ) > 3 ) ? mid( trimmedLine, 4, len( trimmedLine ) ) : ""; var openParen = find( "(", afterAt ); var closeParen = find( ")", afterAt ); @@ -681,7 +681,7 @@ component accessors=true singleton { var colonInParen = find( ":", parenContent ); if ( colonInParen > 1 ) { var atFile = mid( parenContent, 1, colonInParen - 1 ); - var atLine = val( mid( parenContent, colonInParen + 1 ) ); + var atLine = val( mid( parenContent, colonInParen + 1, len( parenContent ) ) ); arrayAppend( curFrames, _buildJavaFrame( atClass, atMethod, atFile, atLine ) ); } else { // Native Method, Unknown Source, etc. @@ -699,7 +699,9 @@ component accessors=true singleton { var beforeColon = mid( trimmedLine, 1, colonPos3 - 1 ); if ( !find( " ", beforeColon ) ) { curType = beforeColon; - curValue = trim( mid( trimmedLine, colonPos3 + 1 ) ); + curValue = ( colonPos3 < len( trimmedLine ) ) + ? trim( mid( trimmedLine, colonPos3 + 1, len( trimmedLine ) ) ) + : ""; } else { // Space before colon — probably a continuation of the message curValue = curValue & " " & trimmedLine; @@ -730,12 +732,14 @@ component accessors=true singleton { required string line, required numeric prefixLen ){ - var afterPrefix = trim( mid( arguments.line, arguments.prefixLen + 1 ) ); + var afterPrefix = ( len( arguments.line ) > arguments.prefixLen ) + ? trim( mid( arguments.line, arguments.prefixLen + 1, len( arguments.line ) ) ) + : ""; var colonPos = find( ":", afterPrefix ); if ( colonPos > 1 ) { return { "type" : trim( mid( afterPrefix, 1, colonPos - 1 ) ), - "value" : trim( mid( afterPrefix, colonPos + 1 ) ) + "value" : trim( mid( afterPrefix, colonPos + 1, len( afterPrefix ) ) ) }; } return { "type" : afterPrefix, "value" : "" }; diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index f1caf10..0dd4056 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -54,8 +54,14 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { try { throw( "Missing tag Context" ); } catch ( any e ) { - var newE = duplicate( e ); - structDelete( newE, "TagContext" ); + // Build a struct manually — duplicate(e) fails on Adobe CF + // and for...in iteration fails on BoxLang + var newE = { + "message" : e.message ?: "", + "detail" : e.detail ?: "", + "type" : e.type ?: "", + "StackTrace" : e.StackTrace ?: "" + }; getLogbox().getRootLogger().error( "Missing tag Context", newE ); } } ); From feb3bdea54e2d20d106e37b1310812e7ba1dc758 Mon Sep 17 00:00:00 2001 From: jclausen <5255645+jclausen@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:04:42 +0000 Subject: [PATCH 20/27] Apply cfformat changes --- models/SentryService.cfc | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 839b0d0..a4ac2b4 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -501,11 +501,11 @@ component accessors=true singleton { }; sentryException[ "exception" ] = { "values" : [ currentException ] }; - // If showJavaStackTrace is enabled AND there's no tagContext, parse the - // Java stack trace and add it as a second (or more) entry in exception.values. - // When tagContext is available, the CFML frames are sufficient — no need - // for the overhead of parsing the raw Java stack trace. - if ( arguments.showJavaStackTrace && !tagContext.len() && len( arguments.exception.StackTrace ) ) { + // If showJavaStackTrace is enabled AND there's no tagContext, parse the + // Java stack trace and add it as a second (or more) entry in exception.values. + // When tagContext is available, the CFML frames are sufficient — no need + // for the overhead of parsing the raw Java stack trace. + if ( arguments.showJavaStackTrace && !tagContext.len() && len( arguments.exception.StackTrace ) ) { var javaExceptions = parseJavaStackTrace( arguments.exception.StackTrace ); for ( var je in javaExceptions ) { arrayAppend( sentryException[ "exception" ].values, je ); @@ -681,7 +681,13 @@ component accessors=true singleton { var colonInParen = find( ":", parenContent ); if ( colonInParen > 1 ) { var atFile = mid( parenContent, 1, colonInParen - 1 ); - var atLine = val( mid( parenContent, colonInParen + 1, len( parenContent ) ) ); + var atLine = val( + mid( + parenContent, + colonInParen + 1, + len( parenContent ) + ) + ); arrayAppend( curFrames, _buildJavaFrame( atClass, atMethod, atFile, atLine ) ); } else { // Native Method, Unknown Source, etc. @@ -700,8 +706,8 @@ component accessors=true singleton { if ( !find( " ", beforeColon ) ) { curType = beforeColon; curValue = ( colonPos3 < len( trimmedLine ) ) - ? trim( mid( trimmedLine, colonPos3 + 1, len( trimmedLine ) ) ) - : ""; + ? trim( mid( trimmedLine, colonPos3 + 1, len( trimmedLine ) ) ) + : ""; } else { // Space before colon — probably a continuation of the message curValue = curValue & " " & trimmedLine; @@ -728,14 +734,17 @@ component accessors=true singleton { * Parse a "Caused by:" or "Suppressed:" exception prefix line. * Returns { type, value }. */ - private struct function _parseExceptionPrefix( - required string line, - required numeric prefixLen - ){ + private struct function _parseExceptionPrefix( required string line, required numeric prefixLen ){ var afterPrefix = ( len( arguments.line ) > arguments.prefixLen ) - ? trim( mid( arguments.line, arguments.prefixLen + 1, len( arguments.line ) ) ) - : ""; - var colonPos = find( ":", afterPrefix ); + ? trim( + mid( + arguments.line, + arguments.prefixLen + 1, + len( arguments.line ) + ) + ) + : ""; + var colonPos = find( ":", afterPrefix ); if ( colonPos > 1 ) { return { "type" : trim( mid( afterPrefix, 1, colonPos - 1 ) ), From fd5d4cec6e3df1c39940726d4dbcbe0a81dd47ee Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Tue, 14 Jul 2026 13:05:34 -0400 Subject: [PATCH 21/27] format and update BL versions --- server-boxlang@1.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server-boxlang@1.json b/server-boxlang@1.json index 65cb3a6..c43ef5f 100644 --- a/server-boxlang@1.json +++ b/server-boxlang@1.json @@ -1,6 +1,6 @@ { "app":{ - "cfengine":"boxlang@1.0.0-snapshot", + "cfengine":"boxlang@1", "serverHomeDirectory":".engine/boxlang" }, "name":"sentry-boxlang@1", @@ -24,6 +24,6 @@ "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9999 -Dboxlang.debugMode=true" }, "scripts":{ - "onServerInitialInstall":"install bx-compat-cfml@be,bx-esapi --noSave" + "onServerInitialInstall":"install bx-compat-cfml,bx-esapi --noSave" } } \ No newline at end of file From 6fd0ae1c0e22b7211ad42baf6a4475de321aab05 Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Tue, 14 Jul 2026 14:22:20 -0400 Subject: [PATCH 22/27] add tagContext creation with pure Java exceptions --- models/SentryService.cfc | 218 +++++++++++++++++------ test-harness/tests/specs/SentryTests.cfc | 209 ++++++++++++++++++++++ 2 files changed, 368 insertions(+), 59 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index a4ac2b4..5cb81a2 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -383,12 +383,8 @@ component accessors=true singleton { arguments.exception.message = arguments.exception.message ?: ""; var sentryExceptionExtra = {}; - var file = ""; - var fileArray = ""; - var currentTemplate = ""; var tagContext = arguments.exception.TagContext; var i = 1; - var st = ""; // If there's no tag context, include the stack trace instead if ( !tagContext.len() ) { @@ -510,6 +506,11 @@ component accessors=true singleton { for ( var je in javaExceptions ) { arrayAppend( sentryException[ "exception" ].values, je ); } + + // Dynamically generate tagContext from CFML/BoxLang template + // references in the Java stack trace, so the frame-building loop + // below can populate source code context for these frames. + tagContext = extractCFMLTagContextFromStackTrace( arguments.exception.StackTrace ); } /* @@ -523,26 +524,17 @@ component accessors=true singleton { var stacki = 0; for ( i = arrayLen( tagContext ); i > 0; i-- ) { stacki++; - var thisTCItem = tagContext[ i ]; - if ( compareNoCase( thisTCItem[ "TEMPLATE" ], currentTemplate ) ) { - fileArray = []; - if ( fileExists( thisTCItem[ "TEMPLATE" ] ) ) { - file = fileOpen( thisTCItem[ "TEMPLATE" ], "read" ); - while ( !fileIsEOF( file ) ) { - arrayAppend( fileArray, fileReadLine( file ) ); - } - fileClose( file ); - } - currentTemplate = thisTCItem[ "TEMPLATE" ]; - } + var thisTCItem = tagContext[ i ]; + var templatePath = normalizeSlashes( thisTCItem[ "TEMPLATE" ] ); + var sourceCtx = readSourceContext( templatePath, thisTCItem[ "LINE" ] ); var thisStackItem = { - "abs_path" : thisTCItem[ "TEMPLATE" ], - "filename" : normalizeSlashes( thisTCItem[ "TEMPLATE" ] ).replace( variables.settings.appRoot, "" ), + "abs_path" : templatePath, + "filename" : templatePath.replace( variables.settings.appRoot, "" ), "lineno" : thisTCItem[ "LINE" ], - "pre_context" : [], - "context_line" : "", - "post_context" : [] + "pre_context" : sourceCtx.pre_context, + "context_line" : sourceCtx.context_line, + "post_context" : sourceCtx.post_context }; // The name of the function being called @@ -551,44 +543,6 @@ component accessors=true singleton { thisStackItem[ "function" ] = functionName; } - // for source code rendering - var fileLen = arrayLen( fileArray ); - var errorLine = thisTCItem[ "LINE" ]; - - if ( errorLine - 3 >= 1 && errorLine - 3 <= fileLen ) { - thisStackItem.pre_context[ 1 ] = fileArray[ errorLine - 3 ]; - } - if ( errorLine - 2 >= 1 && errorLine - 2 <= fileLen ) { - thisStackItem.pre_context[ 2 ] = fileArray[ errorLine - 2 ]; - } - if ( errorLine - 1 >= 1 && errorLine - 1 <= fileLen ) { - thisStackItem.pre_context[ 3 ] = fileArray[ errorLine - 1 ]; - } - - if ( errorLine <= fileLen && fileLen > 0 && errorLine >= 1 ) { - thisStackItem[ "context_line" ] = fileArray[ errorLine ]; - } - - if ( fileLen >= errorLine + 1 ) { - var errorLine1 = errorLine + 1; - - if ( errorLine1 != 0 ) { - thisStackItem.post_context[ 1 ] = fileArray[ errorLine1 ]; - } else if ( fileLen >= errorLine1 + 1 ) { - thisStackItem.post_context[ 1 ] = fileArray[ errorLine1 + 1 ]; - } - } - - if ( fileLen >= errorLine + 2 ) { - var errorLine2 = errorLine + 2; - - if ( errorLine2 != 1 ) { - thisStackItem.post_context[ 2 ] = fileArray[ errorLine2 ]; - } else if ( fileLen >= errorLine2 + 1 ) { - thisStackItem.post_context[ 2 ] = fileArray[ errorLine2 + 1 ]; - } - } - currentException[ "stacktrace" ][ "frames" ][ stacki ] = thisStackItem; } @@ -822,6 +776,152 @@ component accessors=true singleton { return frame; } + /** + * Read source context lines around an error line in a template file. + * Returns a struct with pre_context (3 lines before), context_line, + * and post_context (2 lines after). + * + * @templatePath Absolute path to the template file + * @errorLine The line number where the error occurred (1-based) + */ + private struct function readSourceContext( + required string templatePath, + required numeric errorLine + ){ + var result = { + "pre_context" : [], + "context_line" : "", + "post_context" : [] + }; + + if ( !fileExists( arguments.templatePath ) ) { + return result; + } + + var fileArray = []; + var f = fileOpen( arguments.templatePath, "read" ); + while ( !fileIsEOF( f ) ) { + arrayAppend( fileArray, fileReadLine( f ) ); + } + fileClose( f ); + + var fileLen = arrayLen( fileArray ); + + // Pre-context: 3 lines before the error line + if ( errorLine - 3 >= 1 && errorLine - 3 <= fileLen ) { + result.pre_context[ 1 ] = fileArray[ errorLine - 3 ]; + } + if ( errorLine - 2 >= 1 && errorLine - 2 <= fileLen ) { + result.pre_context[ 2 ] = fileArray[ errorLine - 2 ]; + } + if ( errorLine - 1 >= 1 && errorLine - 1 <= fileLen ) { + result.pre_context[ 3 ] = fileArray[ errorLine - 1 ]; + } + + // Context line (the error line itself) + if ( errorLine <= fileLen && fileLen > 0 && errorLine >= 1 ) { + result.context_line = fileArray[ errorLine ]; + } + + // Post-context: 2 lines after the error line + if ( fileLen >= errorLine + 1 ) { + var errorLine1 = errorLine + 1; + if ( errorLine1 != 0 ) { + result.post_context[ 1 ] = fileArray[ errorLine1 ]; + } else if ( fileLen >= errorLine1 + 1 ) { + result.post_context[ 1 ] = fileArray[ errorLine1 + 1 ]; + } + } + + if ( fileLen >= errorLine + 2 ) { + var errorLine2 = errorLine + 2; + if ( errorLine2 != 1 ) { + result.post_context[ 2 ] = fileArray[ errorLine2 ]; + } else if ( fileLen >= errorLine2 + 1 ) { + result.post_context[ 2 ] = fileArray[ errorLine2 + 1 ]; + } + } + + return result; + } + + /** + * Extract CFML/BoxLang template references from a Java stack trace and + * synthesize TagContext entries so the frame-building loop can populate + * source code context for frames that originated in CFML/BoxLang code. + * + * Matches patterns like: + * - Lucee: at com_example_App_cfc$cf.call(/var/www/App.cfc:42) + * - BoxLang: at boxlang.runtime...(/path/to/Component.bx:15) + * + * Returns an array of structs with keys compatible with the engine-provided + * TagContext (TEMPLATE, LINE, Raw_Trace, type, column, id, codePrintPlain). + * Returns an empty array if no CFML/BoxLang template references are found. + * + * @stackTrace The raw Java stack trace string (typically exception.StackTrace) + */ + private array function extractCFMLTagContextFromStackTrace( required string stackTrace ){ + var result = []; + var cleaned = reReplace( arguments.stackTrace, "\\r", "", "All" ); + var lines = listToArray( cleaned, chr( 10 ) ); + var seenFrames = {}; + + // Regex: match a CFML/BoxLang template path followed by :lineNumber + // inside parentheses — e.g., (/path/to/file.cfm:42) or (C:\app\file.bx:15) + var templatePattern = "\(([^)]+\.(cfm|cfc|bx|bxs|bxm)):(\d+)\)"; + + for ( var line in lines ) { + var trimmedLine = trim( line ); + if ( !len( trimmedLine ) ) { + continue; + } + + // Skip lines that are clearly not CFML-related (e.g., pure Java frames) + // but don't prematurely skip — let the regex decide + var refMatches = reFindNoCase( templatePattern, trimmedLine, 1, true ); + + if ( refMatches.len() && refMatches.pos[ 1 ] > 0 ) { + var templatePath = mid( trimmedLine, refMatches.pos[ 2 ], refMatches.len[ 2 ] ); + var extension = mid( trimmedLine, refMatches.pos[ 3 ], refMatches.len[ 3 ] ); + var lineNumber = val( + mid( trimmedLine, refMatches.pos[ 4 ], refMatches.len[ 4 ] ) + ); + + // Normalize Windows backslash paths to forward slashes + templatePath = normalizeSlashes( templatePath ); + + // Skip invalid line numbers + if ( lineNumber <= 0 ) { + continue; + } + + // Deduplicate: skip if we've already seen this template + line combo + var dedupKey = templatePath & ":" & lineNumber; + if ( structKeyExists( seenFrames, dedupKey ) ) { + continue; + } + seenFrames[ dedupKey ] = true; + + var typeLabel = "cfml"; + if ( listFindNoCase( "bx,bxs,bxm", extension ) ) { + typeLabel = "boxlang"; + } + + arrayAppend( result, { + "TEMPLATE" : templatePath, + "LINE" : lineNumber, + "Raw_Trace" : trimmedLine, + "type" : typeLabel, + "column" : 0, + "id" : "??", + "codePrintPlain" : "" + } ); + } + } + + return result; + } + // recursivley replace any CFC instances with structs function structifyObject( o, name = "" ){ var result = {}; diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index 0dd4056..66477d0 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -419,6 +419,215 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { expect( excValues.len() ).toBe( 1 ); expect( excValues[ 1 ].type ).toBe( "application Error" ); } ); + + // ========== Dynamic TagContext Extraction from Java Stack Trace ========== + + it( "extracts CFML template references from Java stack trace into tagContext", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + // Simulate a Lucee-style stack trace with CFML file references + var testException = { + "message" : "Null Pointer", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.NullPointerException + at com_example_App_cfc$cf.call(/var/www/App.cfc:42) + at com_example_App_cfc$cf.call(/var/www/App.cfc:15) + at com_example_Main_cfc$cf.call(/var/www/Main.cfc:8) + at lucee.runtime.PageContextImpl._doInclude(PageContextImpl.java:1118) + at lucee.runtime.PageContextImpl.execute(PageContextImpl.java:2816)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + // Should have CFML frames on the primary exception + var frames = excValues[ 1 ].stacktrace.frames; + expect( frames.len() ).toBe( 3 ); + + // Frames are in reverse order (bottom of stack first in Sentry) + expect( frames[ 3 ].filename ).toInclude( "Main.cfm" ); + expect( frames[ 3 ].lineno ).toBe( 8 ); + expect( frames[ 2 ].filename ).toInclude( "App.cfc" ); + expect( frames[ 2 ].lineno ).toBe( 15 ); + expect( frames[ 1 ].filename ).toInclude( "App.cfc" ); + expect( frames[ 1 ].lineno ).toBe( 42 ); + } ); + + it( "extracts BoxLang .bx/.bxs/.bxm template references from stack trace", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "BoxLang Error", + "detail" : "", + "type" : "expression", + "TagContext" : [], + "StackTrace" : "java.lang.RuntimeException: boom + at boxlang.runtime.context.RequestContext.execute(/app/services/MyService.bx:55) + at boxlang.runtime.interceptor.InterceptorChain.invoke(/app/handlers/Main.bxs:12) + at boxlang.runtime.module.ModuleLoader.load(/app/modules/Payment.bxm:30) + at java.base/java.lang.Thread.run(Unknown Source)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var frames = payload.exception.values[ 1 ].stacktrace.frames; + + expect( frames.len() ).toBe( 3 ); + expect( frames[ 3 ].filename ).toInclude( "Payment.bxm" ); + expect( frames[ 3 ].lineno ).toBe( 30 ); + expect( frames[ 2 ].filename ).toInclude( "Main.bxs" ); + expect( frames[ 2 ].lineno ).toBe( 12 ); + expect( frames[ 1 ].filename ).toInclude( "MyService.bx" ); + expect( frames[ 1 ].lineno ).toBe( 55 ); + } ); + + it( "returns empty tagContext for pure Java stack traces with no CFML references", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Pure Java", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.IllegalArgumentException: invalid argument + at java.util.Objects.requireNonNull(Objects.java:221) + at java.util.ArrayList.add(ArrayList.java:485) + at org.apache.commons.io.IOUtils.toString(IOUtilsUtils.java:2000) + at com.sun.tools.javac.Main.compile(Main.java:553)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var excValues = payload.exception.values; + + // Primary CFML exception still exists but has no frames + expect( excValues[ 1 ].stacktrace.frames.len() ).toBe( 0 ); + + // Java exception frames are in separate entries from parseJavaStackTrace + expect( excValues.len() ).toBe( 2 ); + expect( excValues[ 2 ].type ).toBe( "java.lang.IllegalArgumentException" ); + } ); + + it( "normalizes Windows backslash paths in stack trace template references", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Windows Path", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.NullPointerException + at com_example_App_cfc$cf.call(C:\wwwroot\app\index.cfm:42) + at lucee.runtime.PageContextImpl.execute(PageContextImpl.java:2816)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var frames = payload.exception.values[ 1 ].stacktrace.frames; + + expect( frames.len() ).toBe( 1 ); + // Should be normalized to forward slashes + expect( frames[ 1 ].filename ).toInclude( "C:/wwwroot/app/index.cfm" ); + expect( frames[ 1 ].lineno ).toBe( 42 ); + } ); + + it( "deduplicates repeated template+line combinations in tagContext", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Duplicate", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.RuntimeException: bang + at com_example_App_cfc$cf.call(/app/index.cfm:10) + at com_example_App_cfc$cf.call(/app/index.cfm:10) + at com_example_Other_cfc$cf.call(/app/other.cfm:20)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var frames = payload.exception.values[ 1 ].stacktrace.frames; + + // Duplicate /app/index.cfm:10 should only appear once + expect( frames.len() ).toBe( 2 ); + } ); + + it( "finds function names via functionLineNums for synthetic tagContext frames", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + // The functionLineNums mock will receive our synthetic Raw_Trace + // and return a function name + service + .getFunctionLineNums() + .$( "findTagContextFunction" ) + .$results( "MyApp.handler", "MyApp.interceptor" ); + + var testException = { + "message" : "Function Names", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.NullPointerException + at com_example_handler_cfc$cf.call(/app/handler.cfc:42) + at com_example_interceptor_cfc$cf.call(/app/interceptor.cfc:15)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var frames = payload.exception.values[ 1 ].stacktrace.frames; + + // functionLineNums.findTagContextFunction() is called during frame building + expect( service.getFunctionLineNums().$count( "findTagContextFunction" ) ).toBe( 2 ); + } ); + + it( "skips invalid line numbers in stack trace template references", function(){ + var service = prepareMock( getSentry() ); + service.setEnabled( true ); + service.$( "post" ); + + var testException = { + "message" : "Bad Lines", + "detail" : "", + "type" : "application", + "TagContext" : [], + "StackTrace" : "java.lang.NullPointerException + at com_example_App_cfc$cf.call(/app/index.cfm:0) + at com_example_App_cfc$cf.call(/app/index.cfm:-1) + at com_example_App_cfc$cf.call(/app/index.cfm:15) + at com_example_App_cfc$cf.call(/app/index.cfm:_invalid_)" + }; + + service.captureException( exception = testException, showJavaStackTrace = true ); + + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var frames = payload.exception.values[ 1 ].stacktrace.frames; + + // Only the valid line 15 should be included + expect( frames.len() ).toBe( 1 ); + expect( frames[ 1 ].lineno ).toBe( 15 ); + } ); } ); } From 7eda4dd29d331cdae5f63d075a064f8bdbae95c9 Mon Sep 17 00:00:00 2001 From: jclausen <5255645+jclausen@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:22:55 +0000 Subject: [PATCH 23/27] Apply cfformat changes --- models/SentryService.cfc | 52 +++++++++++++++--------- test-harness/tests/specs/SentryTests.cfc | 4 +- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 5cb81a2..931f836 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -781,13 +781,10 @@ component accessors=true singleton { * Returns a struct with pre_context (3 lines before), context_line, * and post_context (2 lines after). * - * @templatePath Absolute path to the template file - * @errorLine The line number where the error occurred (1-based) + * @templatePath Absolute path to the template file + * @errorLine The line number where the error occurred (1-based) */ - private struct function readSourceContext( - required string templatePath, - required numeric errorLine - ){ + private struct function readSourceContext( required string templatePath, required numeric errorLine ){ var result = { "pre_context" : [], "context_line" : "", @@ -858,7 +855,7 @@ component accessors=true singleton { * TagContext (TEMPLATE, LINE, Raw_Trace, type, column, id, codePrintPlain). * Returns an empty array if no CFML/BoxLang template references are found. * - * @stackTrace The raw Java stack trace string (typically exception.StackTrace) + * @stackTrace The raw Java stack trace string (typically exception.StackTrace) */ private array function extractCFMLTagContextFromStackTrace( required string stackTrace ){ var result = []; @@ -881,10 +878,22 @@ component accessors=true singleton { var refMatches = reFindNoCase( templatePattern, trimmedLine, 1, true ); if ( refMatches.len() && refMatches.pos[ 1 ] > 0 ) { - var templatePath = mid( trimmedLine, refMatches.pos[ 2 ], refMatches.len[ 2 ] ); - var extension = mid( trimmedLine, refMatches.pos[ 3 ], refMatches.len[ 3 ] ); - var lineNumber = val( - mid( trimmedLine, refMatches.pos[ 4 ], refMatches.len[ 4 ] ) + var templatePath = mid( + trimmedLine, + refMatches.pos[ 2 ], + refMatches.len[ 2 ] + ); + var extension = mid( + trimmedLine, + refMatches.pos[ 3 ], + refMatches.len[ 3 ] + ); + var lineNumber = val( + mid( + trimmedLine, + refMatches.pos[ 4 ], + refMatches.len[ 4 ] + ) ); // Normalize Windows backslash paths to forward slashes @@ -907,15 +916,18 @@ component accessors=true singleton { typeLabel = "boxlang"; } - arrayAppend( result, { - "TEMPLATE" : templatePath, - "LINE" : lineNumber, - "Raw_Trace" : trimmedLine, - "type" : typeLabel, - "column" : 0, - "id" : "??", - "codePrintPlain" : "" - } ); + arrayAppend( + result, + { + "TEMPLATE" : templatePath, + "LINE" : lineNumber, + "Raw_Trace" : trimmedLine, + "type" : typeLabel, + "column" : 0, + "id" : "??", + "codePrintPlain" : "" + } + ); } } diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index 66477d0..909fa89 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -478,8 +478,8 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { service.captureException( exception = testException, showJavaStackTrace = true ); - var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); - var frames = payload.exception.values[ 1 ].stacktrace.frames; + var payload = deserializeJSON( service.$callLog( "post" ).post[ 1 ][ 4 ] ); + var frames = payload.exception.values[ 1 ].stacktrace.frames; expect( frames.len() ).toBe( 3 ); expect( frames[ 3 ].filename ).toInclude( "Payment.bxm" ); From 10c5db5f64bead526185eec86da25b2596c3dcfc Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Tue, 14 Jul 2026 14:28:19 -0400 Subject: [PATCH 24/27] fix mock usage --- test-harness/tests/specs/SentryTests.cfc | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index 909fa89..dfba03e 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -576,12 +576,11 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { service.setEnabled( true ); service.$( "post" ); - // The functionLineNums mock will receive our synthetic Raw_Trace - // and return a function name - service - .getFunctionLineNums() - .$( "findTagContextFunction" ) - .$results( "MyApp.handler", "MyApp.interceptor" ); + // Replace the real functionLineNums property with a mock so + // we can control what findTagContextFunction returns + var mockFLN = createStub(); + mockFLN.$( "findTagContextFunction" ).$results( "MyApp.handler", "MyApp.interceptor" ); + service.$property( propertyName = "functionLineNums", mock = mockFLN ); var testException = { "message" : "Function Names", @@ -599,7 +598,7 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { var frames = payload.exception.values[ 1 ].stacktrace.frames; // functionLineNums.findTagContextFunction() is called during frame building - expect( service.getFunctionLineNums().$count( "findTagContextFunction" ) ).toBe( 2 ); + expect( mockFLN.$count( "findTagContextFunction" ) ).toBe( 2 ); } ); it( "skips invalid line numbers in stack trace template references", function(){ From 25aa365790cce558d8627a1d25893956aebf3df0 Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Tue, 14 Jul 2026 14:41:57 -0400 Subject: [PATCH 25/27] fix reFind usage --- models/SentryService.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 931f836..1186216 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -875,7 +875,7 @@ component accessors=true singleton { // Skip lines that are clearly not CFML-related (e.g., pure Java frames) // but don't prematurely skip — let the regex decide - var refMatches = reFindNoCase( templatePattern, trimmedLine, 1, true ); + var refMatches = reFindNoCase( templatePattern, trimmedLine, 1, true, "all" ); if ( refMatches.len() && refMatches.pos[ 1 ] > 0 ) { var templatePath = mid( From 79437d31e7a22257bbac8d7aa07a5c704793c0e1 Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Tue, 14 Jul 2026 14:48:54 -0400 Subject: [PATCH 26/27] ibid --- models/SentryService.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/SentryService.cfc b/models/SentryService.cfc index 1186216..11bf0fc 100644 --- a/models/SentryService.cfc +++ b/models/SentryService.cfc @@ -875,9 +875,9 @@ component accessors=true singleton { // Skip lines that are clearly not CFML-related (e.g., pure Java frames) // but don't prematurely skip — let the regex decide - var refMatches = reFindNoCase( templatePattern, trimmedLine, 1, true, "all" ); + var refMatches = reFindNoCase( templatePattern, trimmedLine, 1, true ); - if ( refMatches.len() && refMatches.pos[ 1 ] > 0 ) { + if ( refMatches.pos[ 1 ] > 0 ) { var templatePath = mid( trimmedLine, refMatches.pos[ 2 ], From 29b8450ef5591e294dbe1f15e34827de5178d247 Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Tue, 14 Jul 2026 15:16:04 -0400 Subject: [PATCH 27/27] fix test expectations --- test-harness/tests/specs/SentryTests.cfc | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test-harness/tests/specs/SentryTests.cfc b/test-harness/tests/specs/SentryTests.cfc index dfba03e..0ad755d 100644 --- a/test-harness/tests/specs/SentryTests.cfc +++ b/test-harness/tests/specs/SentryTests.cfc @@ -450,13 +450,14 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { var frames = excValues[ 1 ].stacktrace.frames; expect( frames.len() ).toBe( 3 ); - // Frames are in reverse order (bottom of stack first in Sentry) - expect( frames[ 3 ].filename ).toInclude( "Main.cfm" ); - expect( frames[ 3 ].lineno ).toBe( 8 ); + // Sentry frames are oldest-first: frames[1] is the originating + // call, frames[n] is where the exception was thrown + expect( frames[ 1 ].filename ).toInclude( "Main.cfc" ); + expect( frames[ 1 ].lineno ).toBe( 8 ); expect( frames[ 2 ].filename ).toInclude( "App.cfc" ); expect( frames[ 2 ].lineno ).toBe( 15 ); - expect( frames[ 1 ].filename ).toInclude( "App.cfc" ); - expect( frames[ 1 ].lineno ).toBe( 42 ); + expect( frames[ 3 ].filename ).toInclude( "App.cfc" ); + expect( frames[ 3 ].lineno ).toBe( 42 ); } ); it( "extracts BoxLang .bx/.bxs/.bxm template references from stack trace", function(){ @@ -482,12 +483,13 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { var frames = payload.exception.values[ 1 ].stacktrace.frames; expect( frames.len() ).toBe( 3 ); - expect( frames[ 3 ].filename ).toInclude( "Payment.bxm" ); - expect( frames[ 3 ].lineno ).toBe( 30 ); + // Sentry frames are oldest-first + expect( frames[ 1 ].filename ).toInclude( "Payment.bxm" ); + expect( frames[ 1 ].lineno ).toBe( 30 ); expect( frames[ 2 ].filename ).toInclude( "Main.bxs" ); expect( frames[ 2 ].lineno ).toBe( 12 ); - expect( frames[ 1 ].filename ).toInclude( "MyService.bx" ); - expect( frames[ 1 ].lineno ).toBe( 55 ); + expect( frames[ 3 ].filename ).toInclude( "MyService.bx" ); + expect( frames[ 3 ].lineno ).toBe( 55 ); } ); it( "returns empty tagContext for pure Java stack traces with no CFML references", function(){