Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
30 changes: 27 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,15 @@ func (client *Client) CaptureException(exception error, hint *EventHint, scope E
return client.CaptureEvent(event, hint, scope)
}

// CaptureFeedback captures user-provided feedback.
func (client *Client) CaptureFeedback(feedback *Feedback, hint *EventHint, scope EventModifier) *EventID {
event := client.EventFromFeedback(feedback)
if event == nil {
return nil
}
return client.CaptureEvent(event, hint, scope)
}

// CaptureCheckIn captures a check in.
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
event := client.EventFromCheckIn(checkIn, monitorConfig)
Expand Down Expand Up @@ -847,6 +856,20 @@ func (client *Client) EventFromException(exception error, level Level) *Event {
return event
}

// EventFromFeedback creates a new Sentry event from the given feedback.
func (client *Client) EventFromFeedback(feedback *Feedback) *Event {
if feedback == nil {
return nil
}

event := NewEvent()
event.Type = feedbackType
event.Level = LevelInfo
event.Contexts[feedbackType] = feedback.context()

return event
}

Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
// EventFromCheckIn creates a new Sentry event from the given `check_in` instance.
func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
if checkIn == nil {
Expand Down Expand Up @@ -896,8 +919,9 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod

// Transactions are sampled by options.TracesSampleRate or
// options.TracesSampler when they are started. Other events
// (errors, messages) are sampled here. Does not apply to check-ins.
if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
// (errors, messages) are sampled here. Does not apply to check-ins or
// user-submitted feedback.
if event.Type != transactionType && event.Type != checkInType && event.Type != feedbackType && !sample(client.options.SampleRate) {
debuglog.Println("Event dropped due to SampleRate hit.")
client.reportRecorder.RecordOne(report.ReasonSampleRate, event.toCategory())
return nil
Expand Down Expand Up @@ -927,7 +951,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
client.reportRecorder.Record(report.ReasonBeforeSend, ratelimit.CategorySpan, int64(droppedSpans))
}
}
case checkInType: // not a default case, since we shouldn't apply BeforeSend on check-in events
case checkInType, feedbackType: // don't apply the error BeforeSend hook to check-ins or feedback
default:
if client.options.BeforeSend != nil {
if event = client.options.BeforeSend(event, hint); event == nil {
Expand Down
38 changes: 38 additions & 0 deletions feedback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package sentry

// Feedback contains user-provided feedback to send to Sentry.
//
// Message is the only required field. AssociatedEventID may be set to link the
// feedback to a previously captured event.
type Feedback struct {
Message string
Name string
Email string
URL string
Source string
AssociatedEventID EventID
}

func (feedback *Feedback) context() Context {
context := Context{
"message": feedback.Message,
}

if feedback.Name != "" {
context["name"] = feedback.Name
}
if feedback.Email != "" {
context["contact_email"] = feedback.Email
}
if feedback.URL != "" {
context["url"] = feedback.URL
}
if feedback.Source != "" {
context["source"] = feedback.Source
}
if feedback.AssociatedEventID != "" {
context["associated_event_id"] = feedback.AssociatedEventID
}

return context
}
139 changes: 139 additions & 0 deletions feedback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package sentry

import (
"encoding/json"
"testing"

"github.com/getsentry/sentry-go/internal/protocol"
"github.com/google/go-cmp/cmp"
)

func TestCaptureFeedback(t *testing.T) {
client, _, transport := setupClientTest()
scope := NewScope()
scope.SetTag("component", "feedback-form")

eventID := client.CaptureFeedback(&Feedback{
Message: "The save button does not work",
Name: "Jane Doe",
Email: "jane@example.com",
URL: "https://example.com/settings",
Source: "custom-form",
AssociatedEventID: "b81c5be4d31e48959103a1f878a1efcb",
}, nil, scope)

if eventID == nil {
t.Fatal("CaptureFeedback returned a nil event ID")
}

wantContext := Context{
"message": "The save button does not work",
"name": "Jane Doe",
"contact_email": "jane@example.com",
"url": "https://example.com/settings",
"source": "custom-form",
"associated_event_id": EventID("b81c5be4d31e48959103a1f878a1efcb"),
}
if diff := cmp.Diff(wantContext, transport.lastEvent.Contexts[feedbackType]); diff != "" {
t.Errorf("Feedback context mismatch (-want +got):\n%s", diff)
}
if transport.lastEvent.Type != feedbackType {
t.Errorf("Event type = %q, want %q", transport.lastEvent.Type, feedbackType)
}
if transport.lastEvent.Level != LevelInfo {
t.Errorf("Event level = %q, want %q", transport.lastEvent.Level, LevelInfo)
}
if got := transport.lastEvent.Tags["component"]; got != "feedback-form" {
t.Errorf("Scope tag = %q, want %q", got, "feedback-form")
}

item, err := transport.lastEvent.ToEnvelopeItem()
if err != nil {
t.Fatal(err)
}
if item.Header.Type != protocol.EnvelopeItemTypeFeedback {
t.Errorf("Envelope item type = %q, want %q", item.Header.Type, protocol.EnvelopeItemTypeFeedback)
}
var payload map[string]interface{}
if err := json.Unmarshal(item.Payload, &payload); err != nil {
t.Fatal(err)
}
if got := payload["type"]; got != feedbackType {
t.Errorf("Payload type = %q, want %q", got, feedbackType)
}
}

func TestCaptureFeedbackOptionalFieldsAreOmitted(t *testing.T) {
client, scope, transport := setupClientTest()

client.CaptureFeedback(&Feedback{Message: "It works"}, nil, scope)

want := Context{"message": "It works"}
if diff := cmp.Diff(want, transport.lastEvent.Contexts[feedbackType]); diff != "" {
t.Errorf("Feedback context mismatch (-want +got):\n%s", diff)
}
}

func TestCaptureFeedbackNil(t *testing.T) {
client, scope, transport := setupClientTest()

if eventID := client.CaptureFeedback(nil, nil, scope); eventID != nil {
t.Errorf("CaptureFeedback(nil) = %q, want nil", *eventID)
}
if transport.lastEvent != nil {
t.Error("CaptureFeedback(nil) sent an event")
}
}

func TestCaptureFeedbackIgnoresErrorSamplingAndBeforeSend(t *testing.T) {
client, scope, transport := setupClientTest()
client.options.SampleRate = 0
beforeSendCalled := false
client.options.BeforeSend = func(_ *Event, _ *EventHint) *Event {
beforeSendCalled = true
return nil
}

eventID := client.CaptureFeedback(&Feedback{Message: "Feedback"}, nil, scope)

if eventID == nil {
t.Fatal("CaptureFeedback returned a nil event ID")
}
if beforeSendCalled {
t.Error("CaptureFeedback called the error BeforeSend hook")
}
if transport.lastEvent == nil {
t.Error("CaptureFeedback did not send an event")
}
}

func TestEventFromFeedbackMarshalJSONIncludesType(t *testing.T) {
client, _, _ := setupClientTest()
event := client.EventFromFeedback(&Feedback{Message: "Feedback"})

payload, err := json.Marshal(event)
if err != nil {
t.Fatal(err)
}
var got map[string]interface{}
if err := json.Unmarshal(payload, &got); err != nil {
t.Fatal(err)
}
if got["type"] != feedbackType {
t.Errorf("Payload type = %q, want %q", got["type"], feedbackType)
}
}

func TestHubCaptureFeedbackDoesNotChangeLastEventID(t *testing.T) {
hub, _, _ := setupHubTest()
messageID := hub.CaptureMessage("before feedback")

feedbackID := hub.CaptureFeedback(&Feedback{Message: "Feedback"})

if feedbackID == nil {
t.Fatal("CaptureFeedback returned a nil event ID")
}
if got := hub.LastEventID(); got != *messageID {
t.Errorf("LastEventID() = %q, want %q", got, *messageID)
}
}
11 changes: 11 additions & 0 deletions hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,17 @@ func (hub *Hub) CaptureException(exception error) *EventID {
return eventID
}

// CaptureFeedback calls the method of the same name on the currently bound
// Client instance, passing it the top-level Scope.
// Returns EventID if successful, or nil if there's no Scope or Client available.
func (hub *Hub) CaptureFeedback(feedback *Feedback) *EventID {
client, scope := hub.Client(), hub.Scope()
if client == nil || scope == nil {
return nil
}
return client.CaptureFeedback(feedback, nil, scope)
}

// CaptureCheckIn calls the method of the same name on currently bound Client instance
// passing it a top-level Scope.
// Returns CheckInID if the check-in was captured successfully, or nil otherwise.
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down
13 changes: 9 additions & 4 deletions interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const errorType = ""
const eventType = "event"
const transactionType = "transaction"
const checkInType = "check_in"
const feedbackType = "feedback"

var logEvent = struct {
Type string
Expand Down Expand Up @@ -472,6 +473,8 @@ func (e *Event) ToEnvelopeItem() (item *protocol.EnvelopeItem, err error) {
item = protocol.NewTransactionItem(e.GetSpanCount(), eventBody)
case checkInType:
item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody)
case feedbackType:
item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeFeedback, eventBody)
case logEvent.Type:
item = protocol.NewLogItem(len(e.Logs), eventBody)
case traceMetricEvent.Type:
Expand Down Expand Up @@ -570,7 +573,7 @@ func (e *Event) defaultMarshalJSON() ([]byte, error) {
return e.preSerializedMarshalJSON()
}

if e.Type == transactionType {
if e.Type == transactionType || e.Type == feedbackType {
return json.Marshal(struct{ *event }{(*event)(e)})
}
// metrics and logs should be serialized under the same `items` json field.
Expand Down Expand Up @@ -621,16 +624,16 @@ func (e *Event) hasPreSerializedFields() bool {
func (e *Event) preSerializedMarshalJSON() ([]byte, error) {
type event Event

if e.Type == transactionType {
type safeTransaction struct {
if e.Type == transactionType || e.Type == feedbackType {
type safeTypedEvent struct {
*event
Tags json.RawMessage `json:"tags,omitempty"`
Contexts json.RawMessage `json:"contexts,omitempty"`
Breadcrumbs json.RawMessage `json:"breadcrumbs,omitempty"`
Exception json.RawMessage `json:"exception,omitempty"`
User json.RawMessage `json:"user,omitempty"`
}
return json.Marshal(safeTransaction{
return json.Marshal(safeTypedEvent{
event: (*event)(e),
Tags: e.serializedTags,
Contexts: e.serializedContexts,
Expand Down Expand Up @@ -698,6 +701,8 @@ func (e *Event) toCategory() ratelimit.Category {
return ratelimit.CategoryLog
case checkInType:
return ratelimit.CategoryMonitor
case feedbackType:
Comment thread
cursor[bot] marked this conversation as resolved.
return ratelimit.CategoryFeedback
case traceMetricEvent.Type:
return ratelimit.CategoryTraceMetric
default:
Expand Down
1 change: 1 addition & 0 deletions interfaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ func TestEvent_ToCategory(t *testing.T) {
{"transaction", transactionType, ratelimit.CategoryTransaction},
{"log", logEvent.Type, ratelimit.CategoryLog},
{"checkin", checkInType, ratelimit.CategoryMonitor},
{"feedback", feedbackType, ratelimit.CategoryFeedback},
{"unknown", "foobar", ratelimit.CategoryUnknown},
}
for _, tc := range cases {
Expand Down
2 changes: 2 additions & 0 deletions internal/http/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ func categoryFromEnvelope(envelope *protocol.Envelope) ratelimit.Category {
return ratelimit.CategoryTransaction
case protocol.EnvelopeItemTypeCheckIn:
return ratelimit.CategoryMonitor
case protocol.EnvelopeItemTypeFeedback:
return ratelimit.CategoryFeedback
case protocol.EnvelopeItemTypeLog:
return ratelimit.CategoryLog
case protocol.EnvelopeItemTypeAttachment:
Expand Down
9 changes: 9 additions & 0 deletions internal/http/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ func testEnvelope(itemType protocol.EnvelopeItemType) *protocol.Envelope {
}
}

func TestCategoryFromEnvelopeFeedback(t *testing.T) {
got := categoryFromEnvelope(testEnvelope(protocol.EnvelopeItemTypeFeedback))
if got != ratelimit.CategoryFeedback {
t.Errorf("categoryFromEnvelope(feedback) = %q, want %q", got, ratelimit.CategoryFeedback)
}
}

// nolint:gocyclo
func TestAsyncTransport_SendEnvelope(t *testing.T) {
t.Run("invalid DSN", func(t *testing.T) {
Expand Down Expand Up @@ -78,6 +85,7 @@ func TestAsyncTransport_SendEnvelope(t *testing.T) {
{"event", protocol.EnvelopeItemTypeEvent},
{"transaction", protocol.EnvelopeItemTypeTransaction},
{"check-in", protocol.EnvelopeItemTypeCheckIn},
{"feedback", protocol.EnvelopeItemTypeFeedback},
{"log", protocol.EnvelopeItemTypeLog},
{"attachment", protocol.EnvelopeItemTypeAttachment},
}
Expand Down Expand Up @@ -381,6 +389,7 @@ func TestSyncTransport_SendEnvelope(t *testing.T) {
{"event", protocol.EnvelopeItemTypeEvent},
{"transaction", protocol.EnvelopeItemTypeTransaction},
{"check-in", protocol.EnvelopeItemTypeCheckIn},
{"feedback", protocol.EnvelopeItemTypeFeedback},
{"log", protocol.EnvelopeItemTypeLog},
{"attachment", protocol.EnvelopeItemTypeAttachment},
}
Expand Down
1 change: 1 addition & 0 deletions internal/protocol/envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const (
EnvelopeItemTypeEvent EnvelopeItemType = "event"
EnvelopeItemTypeTransaction EnvelopeItemType = "transaction"
EnvelopeItemTypeCheckIn EnvelopeItemType = "check_in"
EnvelopeItemTypeFeedback EnvelopeItemType = "feedback"
EnvelopeItemTypeAttachment EnvelopeItemType = "attachment"
EnvelopeItemTypeLog EnvelopeItemType = "log"
EnvelopeItemTypeTraceMetric EnvelopeItemType = "trace_metric"
Expand Down
6 changes: 6 additions & 0 deletions internal/protocol/envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ func TestEnvelope_ItemsAndSerialization(t *testing.T) {
payload: []byte(`{"check_in_id":"abc123","monitor_slug":"test","status":"ok"}`),
creator: func(p []byte) *EnvelopeItem { return NewEnvelopeItem(EnvelopeItemTypeCheckIn, p) },
},
{
name: "feedback",
itemType: EnvelopeItemTypeFeedback,
payload: []byte(`{"type":"feedback","contexts":{"feedback":{"message":"It works"}}}`),
creator: func(p []byte) *EnvelopeItem { return NewEnvelopeItem(EnvelopeItemTypeFeedback, p) },
},
{
name: "attachment",
itemType: EnvelopeItemTypeAttachment,
Expand Down
Loading