-
-
Notifications
You must be signed in to change notification settings - Fork 19
Feature/java stacktrace exception values #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jclausen
merged 11 commits into
coldbox-modules:development
from
inLeague:feature/java-stacktrace-exception-values
Jul 14, 2026
Merged
Changes from 9 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
05346d9
feat: send Java stack traces as structured exception values in Sentry…
a9d0cce
fix: address review findings for Java stack trace parsing
5727495
test: add Suppressed exception parsing test
3ca293d
fix: rewrite Java stack trace parser to use string operations instead…
b999857
fix: replace regex-based in_app heuristic with string prefix matching
527fa8d
fix: make upstream tests cross-engine compatible
48973bf
style: address review nits from PR #1
6302404
style: apply cfformat formatting to modified files
978e8c0
chore: remove local test server config that was accidentally committed
0993ab3
refactor: address Jon's review on upstream PR #45
39a7ebd
fix: Adobe CF compatibility for mid() and exception handling
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to also populate the tag context, by filtering the lines to |
||
| } | ||
| } | ||
|
|
||
| /* | ||
| * STACKTRACE INTERFACE | ||
|
|
@@ -601,6 +600,222 @@ 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 = []; | ||
| // 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 inException = false; | ||
|
|
||
| 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 ( left( trimmedLine, 3 ) == "..." && reFind( "^\\.\\.\\.\\s+\\d+\\s+more", trimmedLine ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| // "Caused by: ..." — save current exception, start a new one | ||
| if ( left( trimmedLine, 10 ) == "Caused by:" ) { | ||
|
jclausen marked this conversation as resolved.
Outdated
|
||
| // 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; | ||
| continue; | ||
| } | ||
|
|
||
| // "at ..." frame line | ||
| if ( left( trimmedLine, 3 ) == "at " ) { | ||
| 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 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 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; | ||
| } | ||
|
|
||
| // If we haven't hit any "at" lines yet, this is part of the exception header | ||
| if ( !inException ) { | ||
| 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 = trimmedLine; | ||
| } else { | ||
| // Continuation of the message | ||
| curValue = curValue & " " & trimmedLine; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 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 | ||
| 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; | ||
| } | ||
|
|
||
| return frame; | ||
| } | ||
|
|
||
| // recursivley replace any CFC instances with structs | ||
| function structifyObject( o, name = "" ){ | ||
| var result = {}; | ||
|
|
@@ -611,7 +826,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 ) ) { | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.