Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions server/lib/cdpmonitor/cdp_proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ type cdpRuntimeExceptionThrownParams struct {
ExceptionDetails cdpRuntimeExceptionDetails `json:"exceptionDetails"`
}

// cdpRemoteObject mirrors the subset of Runtime.RemoteObject we read off an
// exception payload: Description holds "Error: msg\n at ..." for thrown Errors;
// Value holds the raw value for thrown primitives; UnserializableValue holds
// values CDP cannot JSON-encode (Symbol, BigInt, NaN, ±Infinity).
type cdpRemoteObject struct {
Description string `json:"description,omitempty"`
Value json.RawMessage `json:"value,omitempty"`
UnserializableValue string `json:"unserializableValue,omitempty"`
}

// cdpRuntimeBindingCalledParams mirrors Runtime.bindingCalled params.
type cdpRuntimeBindingCalledParams struct {
Name string `json:"name"`
Expand Down
29 changes: 29 additions & 0 deletions server/lib/cdpmonitor/exception_message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cdpmonitor

import "testing"

func TestExceptionMessage(t *testing.T) {
cases := []struct {
name string
exc string
fallback string
want string
}{
{"error description first line only", `{"className":"Error","description":"Error: boom\n at <anonymous>:1:7"}`, "Uncaught", "Error: boom"},
{"description without newline", `{"description":"TypeError: x is not a function"}`, "Uncaught", "TypeError: x is not a function"},
{"thrown string value", `{"type":"string","value":"just a string"}`, "Uncaught", "just a string"},
{"thrown number value", `{"type":"number","value":42}`, "Uncaught", "42"},
{"thrown null value falls back to text", `{"type":"object","subtype":"null","value":null}`, "Uncaught", "Uncaught"},
{"unserializable bigint", `{"type":"bigint","unserializableValue":"10n"}`, "Uncaught", "10n"},
{"unserializable symbol", `{"type":"symbol","unserializableValue":"Symbol(x)"}`, "Uncaught", "Symbol(x)"},
{"empty exception falls back to text", ``, "Uncaught (in promise)", "Uncaught (in promise)"},
{"malformed json falls back to text", `not json`, "Uncaught", "Uncaught"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := exceptionMessage([]byte(tc.exc), tc.fallback); got != tc.want {
t.Fatalf("exceptionMessage(%q) = %q, want %q", tc.exc, got, tc.want)
}
})
}
}
43 changes: 43 additions & 0 deletions server/lib/cdpmonitor/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,54 @@ import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/kernel/kernel-images/server/lib/events"
oapi "github.com/kernel/kernel-images/server/lib/oapi"
)

// exceptionMessage derives a human-readable message from a Runtime.exceptionThrown
// exception RemoteObject. CDP's exceptionDetails.text is only a generic prefix
// ("Uncaught" / "Uncaught (in promise)"); the real message lives here. Prefers the
// first line of the object's description (e.g. "Error: boom") since the full stack
// is already captured separately, falls back to the thrown value for non-Error
// throws, then to the generic text.
func exceptionMessage(exc json.RawMessage, fallback string) string {
if len(exc) == 0 {
return fallback
}
var o cdpRemoteObject
if err := json.Unmarshal(exc, &o); err != nil {
return fallback
}
if o.Description != "" {
if i := strings.IndexByte(o.Description, '\n'); i >= 0 {
return o.Description[:i]
}
return o.Description

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leading newline yields empty message

Medium Severity

The exceptionMessage function returns an empty string when a CDP exception's description begins with a newline. This occurs because the first-line extraction logic misinterprets a leading newline, resulting in a blank message field in telemetry despite the generic text fallback.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77185e4. Configure here.

}
// Symbol, BigInt, NaN, and ±Infinity throws carry no value/description; CDP
// puts them here.
if o.UnserializableValue != "" {
return o.UnserializableValue
}
if len(o.Value) > 0 {
var v any
if json.Unmarshal(o.Value, &v) == nil {
// A thrown JSON null unmarshals to nil, which %v renders as the
// useless "<nil>"; fall back to the generic text instead.
if v == nil {
return fallback
}
return fmt.Sprintf("%v", v)
}
Comment thread
cursor[bot] marked this conversation as resolved.
return string(o.Value)
}
return fallback
Comment thread
cursor[bot] marked this conversation as resolved.
}

// logUnmarshalErr logs a Debug message when a handler can't parse CDP params.
// These indicate Chrome sent an unexpected params shape, rare and non-actionable
// at Warn/Error level, but useful in verbose mode.
Expand Down Expand Up @@ -184,6 +226,7 @@ func (m *Monitor) handleExceptionThrown(ctx context.Context, p cdpRuntimeExcepti
Url: ptrOf(url),
NavSeq: nseq,
Text: p.ExceptionDetails.Text,
Message: ptrOf(exceptionMessage(p.ExceptionDetails.Exception, p.ExceptionDetails.Text)),
Line: ptrOf(p.ExceptionDetails.LineNumber),
Column: ptrOf(p.ExceptionDetails.ColumnNumber),
SourceUrl: ptrOf(p.ExceptionDetails.URL),
Expand Down
Loading
Loading