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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

**Features**:

- Add `sentry_options_set_before_send_feedback` to filter or enrich user feedback. Feedback does not go through `before_send`. ([#1923](https://github.com/getsentry/sentry-native/pull/1923))

## 0.16.0

**Features**:
Expand Down
15 changes: 15 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,16 @@ discarding_before_send_metric_callback(sentry_value_t metric, void *user_data)
return sentry_value_new_null();
}

static sentry_value_t
discarding_before_send_feedback_callback(
sentry_value_t feedback, sentry_hint_t *hint, void *user_data)
{
(void)hint;
(void)user_data;
sentry_value_decref(feedback);
return sentry_value_new_null();
}

static sentry_value_t
before_breadcrumb_callback(sentry_value_t breadcrumb, void *user_data)
{
Expand Down Expand Up @@ -899,6 +909,11 @@ main(int argc, char **argv)
options, discarding_before_send_metric_callback, NULL);
}

if (has_arg(argc, argv, "discarding-before-send-feedback")) {
sentry_options_set_before_send_feedback(
options, discarding_before_send_feedback_callback, NULL);
}

if (has_arg(argc, argv, "before-breadcrumb")) {
sentry_options_set_before_breadcrumb(
options, before_breadcrumb_callback, NULL);
Expand Down
24 changes: 24 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -3766,6 +3766,30 @@ SENTRY_API void sentry_capture_feedback_with_hint(
SENTRY_API sentry_uuid_t sentry_scope_capture_feedback(
sentry_scope_t *scope, sentry_value_t user_feedback, sentry_hint_t *hint);

/**
* Type of the `before_send_feedback` callback.
*
* The callback takes ownership of the `feedback` event and should usually
* return that same event. In case the feedback should be discarded, the
* callback needs to call `sentry_value_decref` on the provided event and
* return a `sentry_value_new_null()` instead.
*
* The hint is always provided and can be used to add attachments to the event.
*
* Feedback events do not go through the `before_send` callback.
*/
typedef sentry_value_t (*sentry_before_send_feedback_function_t)(
sentry_value_t feedback, sentry_hint_t *hint, void *user_data);

/**
* Sets the `before_send_feedback` callback.
*
* See the `sentry_before_send_feedback_function_t` typedef above for more
* information.
*/
SENTRY_API void sentry_options_set_before_send_feedback(sentry_options_t *opts,
sentry_before_send_feedback_function_t func, void *user_data);

/**
* The status of a Span or Transaction.
*
Expand Down
55 changes: 37 additions & 18 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -838,55 +838,68 @@ prepare_user_feedback(const sentry_options_t *options,
sentry_value_set_by_key(
event, "level", sentry__value_new_level(SENTRY_LEVEL_INFO));

sentry_attachment_t *all_attachments = NULL;
if (hint) {
sentry__attachments_extend(&all_attachments, hint->attachments);
}

if (local_scope) {
SENTRY_DEBUG("merging local scope into feedback event");
sentry__scope_apply_to_event(
local_scope, options, event, SENTRY_SCOPE_NONE);
// must be taken before the scope is freed below
sentry__attachments_extend(&all_attachments, local_scope->attachments);
sentry__scope_free_one_shot(local_scope);
}
SENTRY_WITH_SCOPE (scope) {
SENTRY_DEBUG("merging global scope into feedback event");
sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE);
}

if (options->before_send_feedback_func) {
SENTRY_DEBUG("invoking `before_send_feedback` hook");
event = options->before_send_feedback_func(
event, hint, options->before_send_feedback_data);
if (sentry_value_is_null(event)) {
SENTRY_DEBUG(
"feedback was discarded by the `before_send_feedback` hook");
sentry__client_report_discard(SENTRY_DISCARD_REASON_BEFORE_SEND,
SENTRY_DATA_CATEGORY_FEEDBACK, 1);
sentry__scope_free_one_shot(local_scope);
return NULL;
}
}

sentry__ensure_event_id(event, event_id);

sentry_envelope_t *envelope = sentry__envelope_new();
if (!envelope || !sentry__envelope_add_feedback_event(envelope, event)) {
goto fail;
}

sentry_attachment_t *all_attachments = NULL;
if (hint) {
sentry__attachments_extend(&all_attachments, hint->attachments);
}
if (local_scope) {
sentry__attachments_extend(&all_attachments, local_scope->attachments);
}

SENTRY_WITH_SCOPE (scope) {
const sentry_attachment_t *attachments = scope->attachments;
if (all_attachments) {
// all attachments merged from the hint and the scopes
sentry__attachments_extend(&all_attachments, scope->attachments);
sentry__envelope_add_attachments(
envelope, all_attachments, options);
} else {
// only the global scope has attachments
sentry__envelope_add_attachments(
envelope, scope->attachments, options);
attachments = all_attachments;
}
sentry__envelope_add_attachments(envelope, attachments, options);
if (options->run) {
sentry__cache_attachment_refs(envelope,
all_attachments ? all_attachments : scope->attachments, options,
sentry__cache_attachment_refs(envelope, attachments, options,
options->run->cache_path, options->run->run_path);
}
}

sentry__attachments_free(all_attachments);
sentry__scope_free_one_shot(local_scope);

return envelope;

fail:
SENTRY_WARN("dropping user feedback");
sentry_envelope_free(envelope);
sentry_value_decref(event);
sentry__attachments_free(all_attachments);
sentry__scope_free_one_shot(local_scope);
return NULL;
}

Expand Down Expand Up @@ -1832,6 +1845,12 @@ capture_feedback(sentry_value_t user_feedback, sentry_hint_t *hint,
bool was_sent = false;
SENTRY_WITH_OPTIONS (options) {
was_captured = true;
// Give the hook something to attach to when the caller passed no hint.
if (!hint && options->before_send_feedback_func) {
// A failed allocation is tolerated: operating on a NULL hint
// no-ops.
hint = sentry_hint_new();
}
Comment on lines +1849 to +1853

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: If sentry_hint_new() fails due to out-of-memory, a NULL hint is passed to the before_send_feedback_func callback, silently preventing attachments from being added.
Severity: LOW

Suggested Fix

Check the return value of sentry_hint_new(). If it is NULL, either do not invoke the before_send_feedback_func callback to prevent the silent failure, or explicitly document that the hint may be NULL under out-of-memory conditions and attachments will not be possible.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/sentry_core.c#L1849-L1851

Potential issue: In an out-of-memory (OOM) condition, `sentry_hint_new()` can return
`NULL`. The new code at `src/sentry_core.c:1849~1851` does not check for this `NULL`
return value and passes the `NULL` `hint` to the `before_send_feedback_func` callback.
While attachment functions like `sentry_hint_attach_bytes()` are null-safe and will not
crash, they will silently fail to add attachments. This breaks the documented contract
that the hint can be used to add attachments to the feedback event, leading to a silent
loss of functionality during OOM situations.

Did we get this right? 👍 / 👎 to inform future reviews.

sentry_envelope_t *envelope = prepare_user_feedback(
options, user_feedback, hint, local_scope, &event_id);
if (envelope) {
Expand Down
8 changes: 8 additions & 0 deletions src/sentry_options.c
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,14 @@ sentry_options_set_before_breadcrumb(sentry_options_t *opts,
opts->before_breadcrumb_data = user_data;
}

void
sentry_options_set_before_send_feedback(sentry_options_t *opts,
sentry_before_send_feedback_function_t func, void *user_data)
{
opts->before_send_feedback_func = func;
opts->before_send_feedback_data = user_data;
}

void
sentry_options_set_dsn_n(
sentry_options_t *opts, const char *raw_dsn, size_t raw_dsn_len)
Expand Down
2 changes: 2 additions & 0 deletions src/sentry_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ struct sentry_options_s {
void *before_send_log_data;
sentry_before_breadcrumb_function_t before_breadcrumb_func;
void *before_breadcrumb_data;
sentry_before_send_feedback_function_t before_send_feedback_func;
void *before_send_feedback_data;

/* Experimentally exposed */
double traces_sample_rate;
Expand Down
30 changes: 30 additions & 0 deletions tests/test_integration_client_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,36 @@ def test_client_report_before_send_metric(cmake, httpserver):
)


def test_client_report_before_send_feedback(cmake, httpserver):
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})

httpserver.expect_request("/api/123456/envelope/").respond_with_data("OK")
env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))

# Feedback is discarded by before_send_feedback. The session at
# shutdown carries the client report.
run(
tmp_path,
"sentry_example",
[
"log",
"start-session",
"capture-user-feedback",
"discarding-before-send-feedback",
],
env=env,
)

assert len(httpserver.log) == 1
envelope = Envelope.deserialize(httpserver.log[0][0].get_data())

assert_session(envelope)
assert_client_report(
envelope,
[{"reason": "before_send", "category": "feedback", "quantity": 1}],
)


def test_client_report_before_send_transaction(cmake, httpserver):
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})

Expand Down
Loading
Loading