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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Add HTTP retry with exponential backoff, opt-in via `sentry_options_set_http_retry`. ([#1520](https://github.com/getsentry/sentry-native/pull/1520))
- Store minidump attachments as separate `.dmp` files in the offline cache for direct debugger access. ([#1607](https://github.com/getsentry/sentry-native/pull/1607))
- Enable metrics by default; metrics are now opt-out via `sentry_options_set_enable_metrics(options, false)`. ([#1609](https://github.com/getsentry/sentry-native/pull/1609))
- Track discarded events via client reports. ([#1549](https://github.com/getsentry/sentry-native/pull/1549))

**Fixes**:

Expand Down
21 changes: 21 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -2304,6 +2304,27 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_logs_with_attributes(
SENTRY_EXPERIMENTAL_API int sentry_options_get_logs_with_attributes(
const sentry_options_t *opts);

/**
* Enables or disables client reports.
*
* Client reports allow the SDK to track and report why events were discarded
* before being sent to Sentry (e.g., due to sampling, hooks, rate limiting,
* network or send errors, queue overflow, etc.).
*
* When enabled (the default), client reports are opportunistically attached to
* outgoing envelopes to minimize HTTP requests.
*
* See https://develop.sentry.dev/sdk/telemetry/client-reports/ for details.
*/
SENTRY_API void sentry_options_set_send_client_reports(
sentry_options_t *opts, int val);

/**
* Returns true if client reports are enabled.
*/
SENTRY_API int sentry_options_get_send_client_reports(
const sentry_options_t *opts);

/**
* The potential returns of calling any of the sentry_log_X functions
* - Success means a log was enqueued
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ sentry_target_sources_cwd(sentry
sentry_batcher.c
sentry_batcher.h
sentry_boot.h
sentry_client_report.c
sentry_client_report.h
sentry_core.c
sentry_core.h
sentry_cpu_relax.h
Expand Down
8 changes: 6 additions & 2 deletions src/sentry_batcher.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "sentry_batcher.h"
#include "sentry_alloc.h"
#include "sentry_client_report.h"
#include "sentry_cpu_relax.h"
#include "sentry_options.h"
#include "sentry_utils.h"
Expand All @@ -21,7 +22,8 @@
#endif

sentry_batcher_t *
sentry__batcher_new(sentry_batch_func_t batch_func)
sentry__batcher_new(
sentry_batch_func_t batch_func, sentry_data_category_t data_category)
{
sentry_batcher_t *batcher = SENTRY_MAKE(sentry_batcher_t);
if (!batcher) {
Expand All @@ -30,6 +32,7 @@ sentry__batcher_new(sentry_batch_func_t batch_func)
memset(batcher, 0, sizeof(sentry_batcher_t));
batcher->refcount = 1;
batcher->batch_func = batch_func;
batcher->data_category = data_category;
batcher->thread_state = (long)SENTRY_BATCHER_THREAD_STOPPED;
sentry__waitable_flag_init(&batcher->request_flush);
sentry__thread_init(&batcher->batching_thread);
Expand Down Expand Up @@ -301,7 +304,8 @@ sentry__batcher_enqueue(sentry_batcher_t *batcher, sentry_value_t item)
// Buffer is already full, roll back our increments and retry or drop.
sentry__atomic_fetch_and_add(&active->adding, -1);
if (attempt == ENQUEUE_MAX_RETRIES) {
// TODO report this (e.g. client reports)
sentry__client_report_discard(SENTRY_DISCARD_REASON_QUEUE_OVERFLOW,
batcher->data_category, 1);
return false;
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/sentry_batcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define SENTRY_BATCHER_H_INCLUDED

#include "sentry_boot.h"
#include "sentry_client_report.h"
#include "sentry_database.h"
#include "sentry_envelope.h"
#include "sentry_sync.h"
Expand Down Expand Up @@ -44,6 +45,7 @@ typedef struct {
sentry_waitable_flag_t request_flush; // level-triggered flush flag
sentry_threadid_t batching_thread; // the batching thread
sentry_batch_func_t batch_func; // function to add items to envelope
sentry_data_category_t data_category; // for client report discard tracking
sentry_dsn_t *dsn;
sentry_transport_t *transport;
sentry_run_t *run;
Expand All @@ -57,7 +59,8 @@ typedef struct {

#define SENTRY_BATCHER_REF_INIT { NULL, 0 }

sentry_batcher_t *sentry__batcher_new(sentry_batch_func_t batch_func);
sentry_batcher_t *sentry__batcher_new(
sentry_batch_func_t batch_func, sentry_data_category_t data_category);

/**
* Acquires a reference to the batcher behind `ref`, atomically incrementing
Expand Down
214 changes: 214 additions & 0 deletions src/sentry_client_report.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
#include "sentry_client_report.h"
#include "sentry_alloc.h"
#include "sentry_envelope.h"
#include "sentry_json.h"
#include "sentry_ratelimiter.h"
#include "sentry_string.h"
#include "sentry_sync.h"
#include "sentry_utils.h"
#include "sentry_value.h"

// Counters for discarded events, indexed by [reason][category]
static volatile long g_discard_counts[SENTRY_DISCARD_REASON_MAX]
[SENTRY_DATA_CATEGORY_MAX]
= { { 0 } };

static const char *
discard_reason_to_string(sentry_discard_reason_t reason)
{
switch (reason) {
case SENTRY_DISCARD_REASON_QUEUE_OVERFLOW:
return "queue_overflow";
case SENTRY_DISCARD_REASON_RATELIMIT_BACKOFF:
return "ratelimit_backoff";
case SENTRY_DISCARD_REASON_NETWORK_ERROR:
return "network_error";
case SENTRY_DISCARD_REASON_SAMPLE_RATE:
return "sample_rate";
case SENTRY_DISCARD_REASON_BEFORE_SEND:
return "before_send";
case SENTRY_DISCARD_REASON_EVENT_PROCESSOR:
return "event_processor";
case SENTRY_DISCARD_REASON_SEND_ERROR:
return "send_error";
case SENTRY_DISCARD_REASON_MAX:
default:
return "unknown";
}
}

static const char *
data_category_to_string(sentry_data_category_t category)
{
switch (category) {
case SENTRY_DATA_CATEGORY_ERROR:
return "error";
case SENTRY_DATA_CATEGORY_SESSION:
return "session";
case SENTRY_DATA_CATEGORY_TRANSACTION:
return "transaction";
case SENTRY_DATA_CATEGORY_ATTACHMENT:
return "attachment";
case SENTRY_DATA_CATEGORY_LOG_ITEM:
return "log_item";
case SENTRY_DATA_CATEGORY_FEEDBACK:
return "feedback";
case SENTRY_DATA_CATEGORY_TRACE_METRIC:
return "trace_metric";
case SENTRY_DATA_CATEGORY_MAX:
default:
return "unknown";
}
}

static void
reset_discard_counts(
long counts[SENTRY_DISCARD_REASON_MAX][SENTRY_DATA_CATEGORY_MAX])
{
for (int r = 0; r < SENTRY_DISCARD_REASON_MAX; r++) {
for (int c = 0; c < SENTRY_DATA_CATEGORY_MAX; c++) {
if (counts) {
if (counts[r][c] > 0) {
sentry__atomic_fetch_and_add(
(long *)&g_discard_counts[r][c], -counts[r][c]);
}
} else {
sentry__atomic_store((long *)&g_discard_counts[r][c], 0);
}
}
}
}

void
sentry__client_report_discard(sentry_discard_reason_t reason,
sentry_data_category_t category, long quantity)
{
if (reason >= SENTRY_DISCARD_REASON_MAX
|| category >= SENTRY_DATA_CATEGORY_MAX || quantity <= 0) {
return;
}

sentry__atomic_fetch_and_add(
(long *)&g_discard_counts[reason][category], quantity);
}

bool
sentry__client_report_has_pending(void)
{
for (int r = 0; r < SENTRY_DISCARD_REASON_MAX; r++) {
for (int c = 0; c < SENTRY_DATA_CATEGORY_MAX; c++) {
if (sentry__atomic_fetch((long *)&g_discard_counts[r][c]) > 0) {
return true;
}
}
}
return false;
}

sentry_envelope_item_t *
sentry__client_report_into_envelope(sentry_envelope_t *envelope)
{
if (!envelope || !sentry__client_report_has_pending()) {
return NULL;
}

sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL);
if (!jw) {
return NULL;
}

sentry__jsonwriter_write_object_start(jw);
sentry__jsonwriter_write_key(jw, "timestamp");
char *timestamp = sentry__usec_time_to_iso8601(sentry__usec_time());
sentry__jsonwriter_write_str(jw, timestamp);
sentry_free(timestamp);

long counts[SENTRY_DISCARD_REASON_MAX][SENTRY_DATA_CATEGORY_MAX];

sentry__jsonwriter_write_key(jw, "discarded_events");
sentry__jsonwriter_write_list_start(jw);
for (int r = 0; r < SENTRY_DISCARD_REASON_MAX; r++) {
for (int c = 0; c < SENTRY_DATA_CATEGORY_MAX; c++) {
long count = sentry__atomic_fetch((long *)&g_discard_counts[r][c]);
counts[r][c] = count;
if (count > 0) {
sentry__jsonwriter_write_object_start(jw);
sentry__jsonwriter_write_key(jw, "reason");
sentry__jsonwriter_write_str(jw, discard_reason_to_string(r));
sentry__jsonwriter_write_key(jw, "category");
sentry__jsonwriter_write_str(jw, data_category_to_string(c));
sentry__jsonwriter_write_key(jw, "quantity");
sentry__jsonwriter_write_int64(jw, count);
sentry__jsonwriter_write_object_end(jw);
}
}
}
sentry__jsonwriter_write_list_end(jw);
sentry__jsonwriter_write_object_end(jw);

size_t payload_len = 0;
char *payload = sentry__jsonwriter_into_string(jw, &payload_len);
if (!payload) {
return NULL;
}

sentry_envelope_item_t *item = sentry__envelope_add_from_buffer(
envelope, payload, payload_len, "client_report");
sentry_free(payload);

if (!item) {
return NULL;
}

reset_discard_counts(counts);
return item;
}

sentry_data_category_t
sentry__item_type_to_data_category(const char *ty)
{
if (sentry__string_eq(ty, "session")) {
return SENTRY_DATA_CATEGORY_SESSION;
} else if (sentry__string_eq(ty, "transaction")) {
return SENTRY_DATA_CATEGORY_TRANSACTION;
} else if (sentry__string_eq(ty, "attachment")) {
return SENTRY_DATA_CATEGORY_ATTACHMENT;
} else if (sentry__string_eq(ty, "log")) {
return SENTRY_DATA_CATEGORY_LOG_ITEM;
} else if (sentry__string_eq(ty, "feedback")) {
return SENTRY_DATA_CATEGORY_FEEDBACK;
} else if (sentry__string_eq(ty, "trace_metric")) {
return SENTRY_DATA_CATEGORY_TRACE_METRIC;
}
return SENTRY_DATA_CATEGORY_ERROR;
}

void
sentry__client_report_discard_envelope(const sentry_envelope_t *envelope,
sentry_discard_reason_t reason, const sentry_rate_limiter_t *rl)
{
size_t count = sentry__envelope_get_item_count(envelope);
for (size_t i = 0; i < count; i++) {
const sentry_envelope_item_t *item
= sentry__envelope_get_item(envelope, i);
const char *ty = sentry_value_as_string(
sentry__envelope_item_get_header(item, "type"));
int rl_category = sentry__envelope_item_type_to_rl_category(ty);
// internal items (e.g. client_report) bypass rate limiting
if (rl_category < 0) {
continue;
}
// already recorded as ratelimit_backoff
if (rl && sentry__rate_limiter_is_disabled(rl, rl_category)) {
continue;
}
sentry__client_report_discard(
reason, sentry__item_type_to_data_category(ty), 1);
}
}

void
sentry__client_report_reset(void)
{
reset_discard_counts(NULL);
}
74 changes: 74 additions & 0 deletions src/sentry_client_report.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#ifndef SENTRY_CLIENT_REPORT_H_INCLUDED
#define SENTRY_CLIENT_REPORT_H_INCLUDED

#include "sentry_boot.h"
#include "sentry_ratelimiter.h"

/**
* Discard reasons as specified in the Sentry SDK telemetry documentation.
* https://develop.sentry.dev/sdk/telemetry/client-reports/
*/
typedef enum {
SENTRY_DISCARD_REASON_QUEUE_OVERFLOW,
SENTRY_DISCARD_REASON_RATELIMIT_BACKOFF,
SENTRY_DISCARD_REASON_NETWORK_ERROR,
SENTRY_DISCARD_REASON_SAMPLE_RATE,
SENTRY_DISCARD_REASON_BEFORE_SEND,
SENTRY_DISCARD_REASON_EVENT_PROCESSOR,
SENTRY_DISCARD_REASON_SEND_ERROR,
SENTRY_DISCARD_REASON_MAX
} sentry_discard_reason_t;

/**
* Data categories for tracking discarded events.
* These match the rate limiting categories defined at:
* https://develop.sentry.dev/sdk/expected-features/rate-limiting/#definitions
*/
typedef enum {
SENTRY_DATA_CATEGORY_ERROR,
SENTRY_DATA_CATEGORY_SESSION,
SENTRY_DATA_CATEGORY_TRANSACTION,
SENTRY_DATA_CATEGORY_ATTACHMENT,
SENTRY_DATA_CATEGORY_LOG_ITEM,
SENTRY_DATA_CATEGORY_FEEDBACK,
SENTRY_DATA_CATEGORY_TRACE_METRIC,
SENTRY_DATA_CATEGORY_MAX
} sentry_data_category_t;

/**
* Record a discarded event with the given reason and category.
* This function is thread-safe using atomic operations.
*/
sentry_data_category_t sentry__item_type_to_data_category(const char *ty);

void sentry__client_report_discard(sentry_discard_reason_t reason,
sentry_data_category_t category, long quantity);

/**
* Check if there are any pending discards to report.
* Returns true if there are discards, false otherwise.
*/
bool sentry__client_report_has_pending(void);

/**
* Create a client report envelope item and add it to the given envelope.
* This atomically flushes all pending discard counters.
* Returns the envelope item if added successfully, NULL otherwise.
*/
struct sentry_envelope_item_s *sentry__client_report_into_envelope(
sentry_envelope_t *envelope);

/**
* Record discards for all non-internal items in the envelope.
* Skips client_report items. Each item is mapped to its data category.
*/
void sentry__client_report_discard_envelope(const sentry_envelope_t *envelope,
sentry_discard_reason_t reason, const sentry_rate_limiter_t *rl);

/**
* Reset all client report counters to zero.
* Called during SDK initialization to ensure a clean state.
*/
void sentry__client_report_reset(void);

#endif
Loading
Loading