fix(serialization): pre-serialize mutable event fields to prevent race panics #1214
Merged
fix(serialization): pre-serialize mutable event fields to prevent race panics #1214
Conversation
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛
Internal Changes 🔧Deps
Other
🤖 This preview updates automatically when you update the PR. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Transaction spans silently dropped in pre-serialized path
- I removed the
safeTransactionshadowSpansfield so transaction marshaling now keepsEvent.Spansin the pre-serialized path.
- I removed the
- ✅ Fixed: User silently dropped when User.Data is empty
- I changed
MakeSerializationSafeto serializeUserwhenever it is non-empty (!e.User.IsEmpty()), preserving users withoutData.
- I changed
Or push these changes by commenting:
@cursor push 2b9dc34904
Preview (2b9dc34904)
diff --git a/interfaces.go b/interfaces.go
--- a/interfaces.go
+++ b/interfaces.go
@@ -641,7 +641,6 @@
Contexts json.RawMessage `json:"contexts,omitempty"`
Breadcrumbs json.RawMessage `json:"breadcrumbs,omitempty"`
Exception json.RawMessage `json:"exception,omitempty"`
- Spans json.RawMessage `json:"spans,omitempty"`
User json.RawMessage `json:"user,omitempty"`
}
return json.Marshal(safeTransaction{
@@ -904,7 +903,7 @@
}
}
- if len(e.User.Data) > 0 {
+ if !e.User.IsEmpty() {
if b, err := json.Marshal(e.User); err == nil {
e.serializedUser = b
}
diff --git a/interfaces_test.go b/interfaces_test.go
--- a/interfaces_test.go
+++ b/interfaces_test.go
@@ -1028,6 +1028,73 @@
}
})
+ t.Run("pre-serializes User when Data is empty", func(t *testing.T) {
+ event := &Event{
+ Extra: map[string]interface{}{"key": "value"},
+ User: User{ID: "1", Email: "user@example.com"},
+ }
+ event.MakeSerializationSafe()
+
+ if event.serializedUser == nil {
+ t.Fatal("serializedUser should be set")
+ }
+
+ jsonData, err := json.Marshal(event)
+ if err != nil {
+ t.Fatalf("marshal failed: %v", err)
+ }
+
+ var got map[string]interface{}
+ if err := json.Unmarshal(jsonData, &got); err != nil {
+ t.Fatalf("unmarshal failed: %v", err)
+ }
+
+ user, ok := got["user"].(map[string]interface{})
+ if !ok {
+ t.Fatalf("expected user field in JSON, got %s", jsonData)
+ }
+ if user["id"] != "1" {
+ t.Errorf("expected user.id=1, got %v", user["id"])
+ }
+ if user["email"] != "user@example.com" {
+ t.Errorf("expected user.email=user@example.com, got %v", user["email"])
+ }
+ })
+
+ t.Run("pre-serializes transaction spans", func(t *testing.T) {
+ event := &Event{
+ Type: transactionType,
+ Contexts: map[string]Context{
+ "trace": TraceContext{
+ TraceID: TraceIDFromHex("90d57511038845dcb4164a70fc3a7fdb"),
+ SpanID: SpanIDFromHex("f7f3fd754a9040eb"),
+ }.Map(),
+ },
+ Spans: []*Span{{
+ TraceID: TraceIDFromHex("90d57511038845dcb4164a70fc3a7fdb"),
+ SpanID: SpanIDFromHex("4aaf45ea7db94520"),
+ StartTime: time.Unix(1, 0).UTC(),
+ EndTime: time.Unix(2, 0).UTC(),
+ }},
+ }
+ event.MakeSerializationSafe()
+
+ jsonData, err := json.Marshal(event)
+ if err != nil {
+ t.Fatalf("marshal failed: %v", err)
+ }
+
+ var got map[string]interface{}
+ if err := json.Unmarshal(jsonData, &got); err != nil {
+ t.Fatalf("unmarshal failed: %v", err)
+ }
+
+ spans, ok := got["spans"].([]interface{})
+ if !ok || len(spans) != 1 {
+ t.Fatalf("expected one span in JSON, got %s", jsonData)
+ }
+ })
+
t.Run("marshaled output matches original event", func(t *testing.T) {
event := &Event{
EventID: "12345678901234567890123456789012",
Contributor
Author
Applied via @cursor push command
sl0thentr0py
approved these changes
Mar 3, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

Description
This PR Fixes #1146.
json.Marshal on the background scheduler goroutine can panic with "index out of range" when user code concurrently mutates Event fields containing maps or slices (Extra, Contexts, Breadcrumbs, Exception,
User.Data).
Instead of deep-copying these fields, this pre-serializes them to json.RawMessage in MakeSerializationSafe() on the calling goroutine. The background MarshalJSON then emits the frozen bytes via a shadow struct, avoiding any access to the live shared data.
Key decisions:
Issues
Changelog Entry Instructions
To add a custom changelog entry, uncomment the section above. Supports:
For more details: custom changelog entries
Reminders
feat:,fix:,ref:,meta:)