Skip to content
Draft
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
21 changes: 21 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,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, before_send hooks,
* rate limiting, network errors, 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
4 changes: 3 additions & 1 deletion src/sentry_batcher.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "sentry_batcher.h"
#include "sentry_client_report.h"
#include "sentry_cpu_relax.h"
#include "sentry_options.h"

Expand Down Expand Up @@ -204,7 +205,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,
SENTRY_DATA_CATEGORY_LOG_ITEM, 1);
return false;
}
}
Expand Down
210 changes: 210 additions & 0 deletions src/sentry_client_report.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#include "sentry_client_report.h"
#include "sentry_alloc.h"
#include "sentry_envelope.h"
#include "sentry_json.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_SPAN:
return "span";
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_MAX:
default:
return "unknown";
}
}

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 reason = 0; reason < SENTRY_DISCARD_REASON_MAX; reason++) {
for (int category = 0; category < SENTRY_DATA_CATEGORY_MAX;
category++) {
if (sentry__atomic_fetch(
(long *)&g_discard_counts[reason][category])
> 0) {
return true;
}
}
}
return false;
}

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

long counts[SENTRY_DISCARD_REASON_MAX][SENTRY_DATA_CATEGORY_MAX]
= { { 0 } };
bool has_data = false;

for (int reason = 0; reason < SENTRY_DISCARD_REASON_MAX; reason++) {
for (int category = 0; category < SENTRY_DATA_CATEGORY_MAX;
category++) {
long count = sentry__atomic_store(
(long *)&g_discard_counts[reason][category], 0);
counts[reason][category] = count;
if (count > 0) {
has_data = true;
}
}
}

if (!has_data) {
return NULL;
}

sentry_value_t client_report = sentry_value_new_object();

sentry_value_set_by_key(client_report, "timestamp",
sentry__value_new_string_owned(
sentry__usec_time_to_iso8601(sentry__usec_time())));

sentry_value_t discarded_events = sentry_value_new_list();

for (int reason = 0; reason < SENTRY_DISCARD_REASON_MAX; reason++) {
for (int category = 0; category < SENTRY_DATA_CATEGORY_MAX;
category++) {
long count = counts[reason][category];
if (count > 0) {
sentry_value_t entry = sentry_value_new_object();
sentry_value_set_by_key(entry, "reason",
sentry_value_new_string(discard_reason_to_string(
(sentry_discard_reason_t)reason)));
sentry_value_set_by_key(entry, "category",
sentry_value_new_string(data_category_to_string(
(sentry_data_category_t)category)));
sentry_value_set_by_key(
entry, "quantity", sentry_value_new_int32((int32_t)count));
sentry_value_append(discarded_events, entry);
}
}
}

sentry_value_set_by_key(
client_report, "discarded_events", discarded_events);

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

sentry__jsonwriter_write_value(jw, client_report);
size_t payload_len = 0;
char *payload = sentry__jsonwriter_into_string(jw, &payload_len);
sentry_value_decref(client_report);

if (!payload) {
return NULL;
}

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

return item;
}

static sentry_data_category_t
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;
}
return SENTRY_DATA_CATEGORY_ERROR;
}

void
sentry__client_report_discard_envelope(
const sentry_envelope_t *envelope, sentry_discard_reason_t reason)
{
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"));
if (sentry__string_eq(ty, "client_report")) {
continue;
}
sentry__client_report_discard(
reason, item_type_to_data_category(ty), 1);
}
}

void
sentry__client_report_reset(void)
{
for (int reason = 0; reason < SENTRY_DISCARD_REASON_MAX; reason++) {
for (int category = 0; category < SENTRY_DATA_CATEGORY_MAX;
category++) {
sentry__atomic_store(
(long *)&g_discard_counts[reason][category], 0);
}
}
}
71 changes: 71 additions & 0 deletions src/sentry_client_report.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#ifndef SENTRY_CLIENT_REPORT_H_INCLUDED
#define SENTRY_CLIENT_REPORT_H_INCLUDED

#include "sentry_boot.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_SPAN,
SENTRY_DATA_CATEGORY_ATTACHMENT,
SENTRY_DATA_CATEGORY_LOG_ITEM,
SENTRY_DATA_CATEGORY_FEEDBACK,
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.
*/
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);

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

#endif
9 changes: 9 additions & 0 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "sentry_attachment.h"
#include "sentry_backend.h"
#include "sentry_client_report.h"
#include "sentry_core.h"
#include "sentry_database.h"
#include "sentry_envelope.h"
Expand Down Expand Up @@ -246,6 +247,8 @@ sentry_init(sentry_options_t *options)
g_last_crash = sentry__has_crash_marker(options);
g_options = options;

sentry__client_report_reset();

// *after* setting the global options, trigger a scope and consent flush,
// since at least crashpad needs that. At this point we also freeze the
// `client_sdk` in the `scope` because some downstream SDKs want to override
Expand Down Expand Up @@ -625,6 +628,8 @@ sentry__capture_event(sentry_value_t event, sentry_scope_t *local_scope)
bool should_skip = !sentry__roll_dice(options->sample_rate);
if (should_skip) {
SENTRY_INFO("throwing away event due to sample rate");
sentry__client_report_discard(SENTRY_DISCARD_REASON_SAMPLE_RATE,
SENTRY_DATA_CATEGORY_ERROR, 1);
sentry_envelope_free(envelope);
} else {
sentry__capture_envelope(options->transport, envelope);
Expand Down Expand Up @@ -718,6 +723,8 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event,
= options->before_send_func(event, NULL, options->before_send_data);
if (sentry_value_is_null(event)) {
SENTRY_DEBUG("event was discarded by the `before_send` hook");
sentry__client_report_discard(SENTRY_DISCARD_REASON_BEFORE_SEND,
SENTRY_DATA_CATEGORY_ERROR, 1);
return NULL;
}
}
Expand Down Expand Up @@ -769,6 +776,8 @@ sentry__prepare_transaction(const sentry_options_t *options,
if (sentry_value_is_null(transaction)) {
SENTRY_DEBUG(
"transaction was discarded by the `before_transaction` hook");
sentry__client_report_discard(SENTRY_DISCARD_REASON_BEFORE_SEND,
SENTRY_DATA_CATEGORY_TRANSACTION, 1);
return NULL;
}
}
Expand Down
Loading
Loading