Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

## Unreleased

**Features**:

- Report the byte size of discarded logs and metrics in client reports under the `log_byte` and `trace_metric_byte` categories, alongside the existing `log_item` and `trace_metric` counts.

**Fixes**:
- Honor checks before launching crash reporter ([#1906](https://github.com/getsentry/sentry-native/pull/1906))
- Client reports now count every log in a discarded batch instead of counting the whole envelope item as a single discard, so dropping a batch of 100 logs is reported as 100 discarded `log_item`s rather than 1. The same applies to batched metrics.

## 0.16.0

Expand Down
6 changes: 4 additions & 2 deletions src/sentry_batcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "sentry_cpu_relax.h"
#include "sentry_options.h"
#include "sentry_utils.h"
#include "sentry_value.h"

// The batcher thread sleeps for this interval between flush cycles.
// When the timer fires and there are items in the buffer, they are flushed
Expand Down Expand Up @@ -311,8 +312,9 @@ sentry__batcher_enqueue(sentry_batcher_t *batcher, sentry_value_t item)
|| sentry__atomic_fetch(&batcher->active_idx) != active_idx) {
continue;
}
sentry__client_report_discard(
SENTRY_DISCARD_REASON_QUEUE_OVERFLOW, batcher->data_category, 1);
sentry__client_report_discard_with_bytes(
SENTRY_DISCARD_REASON_QUEUE_OVERFLOW, batcher->data_category, 1,
(long)sentry__value_estimate_serialized_size(item));
return false;
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/sentry_client_report.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ sentry__client_report_discard(sentry_discard_reason_t reason,
(long *)&g_discard_counts[reason][category], quantity);
}

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

switch (category) {
case SENTRY_DATA_CATEGORY_LOG_ITEM:
sentry__client_report_discard(
reason, SENTRY_DATA_CATEGORY_LOG_BYTE, bytes);
break;
case SENTRY_DATA_CATEGORY_TRACE_METRIC:
sentry__client_report_discard(
reason, SENTRY_DATA_CATEGORY_TRACE_METRIC_BYTE, bytes);
break;
default:
// all other categories have no byte counterpart
break;
}
}

bool
sentry__client_report_has_pending(void)
{
Expand Down
20 changes: 20 additions & 0 deletions src/sentry_client_report.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ typedef enum {
* Data categories for tracking discarded events.
* These match the rate limiting categories defined at:
* https://develop.sentry.dev/sdk/expected-features/rate-limiting/#definitions
*
* The `_BYTE` categories track the byte size of discarded telemetry alongside
* its item count, see
* https://develop.sentry.dev/sdk/telemetry/client-reports/#log-byte-outcomes
*/
typedef enum {
SENTRY_DATA_CATEGORY_ERROR,
Expand All @@ -32,11 +36,17 @@ typedef enum {
SENTRY_DATA_CATEGORY_FEEDBACK,
SENTRY_DATA_CATEGORY_TRACE_METRIC,
SENTRY_DATA_CATEGORY_REPLAY,
SENTRY_DATA_CATEGORY_LOG_BYTE,
SENTRY_DATA_CATEGORY_TRACE_METRIC_BYTE,
SENTRY_DATA_CATEGORY_MAX
} sentry_data_category_t;

/**
* Consumed discard counts, used to restore them on send failure.
*
* NOTE: `long` is 32-bit on Windows, so the `_BYTE` categories saturate at 2GiB
* of discarded telemetry between two client reports. Since the counters are
* flushed with every outgoing envelope, reaching that is not realistic.
*/
typedef struct {
long counts[SENTRY_DISCARD_REASON_MAX][SENTRY_DATA_CATEGORY_MAX];
Expand All @@ -49,6 +59,16 @@ typedef struct {
void sentry__client_report_discard(sentry_discard_reason_t reason,
sentry_data_category_t category, long quantity);

/**
* Record `quantity` discarded items of `category`, plus `bytes` under the
* category's byte counterpart (`log_byte` for logs, `trace_metric_byte` for
* metrics). `bytes` is ignored for categories that have no byte counterpart,
* so callers that discard items of an unknown category can use this
* unconditionally.
*/
void sentry__client_report_discard_with_bytes(sentry_discard_reason_t reason,
sentry_data_category_t category, long quantity, long bytes);

/**
* Check if there are any pending discards to report.
*/
Expand Down
39 changes: 30 additions & 9 deletions src/sentry_envelope.c
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,29 @@ item_type_to_data_category(const char *ty)
return SENTRY_DATA_CATEGORY_ERROR;
}

/**
* Records a client report discard for a single envelope item.
*
* Batched items (logs, metrics) carry more than one item in a single payload,
* so the count comes from their `item_count` header. The already-serialized
* payload gives us an exact byte size for the `*_byte` categories.
*/
static void
discard_envelope_item(
const sentry_envelope_item_t *item, sentry_discard_reason_t reason)
{
const char *ty = sentry_value_as_string(
sentry_value_get_by_key(item->headers, "type"));
const sentry_value_t item_count
= sentry_value_get_by_key(item->headers, "item_count");
const long quantity = sentry_value_is_null(item_count)
? 1
: (long)sentry_value_as_int32(item_count);

sentry__client_report_discard_with_bytes(reason,
item_type_to_data_category(ty), quantity, (long)item->payload_len);
}

bool
sentry__envelope_can_add_client_report(
const sentry_envelope_t *envelope, const sentry_rate_limiter_t *rl)
Expand Down Expand Up @@ -869,11 +892,8 @@ sentry_envelope_serialize_ratelimited(const sentry_envelope_t *envelope,
// category < 0 means the item should bypass rate limiting
if (category >= 0
&& sentry__rate_limiter_is_disabled(rl, category)) {
const char *ty = sentry_value_as_string(
sentry_value_get_by_key(item->headers, "type"));
sentry__client_report_discard(
SENTRY_DISCARD_REASON_RATELIMIT_BACKOFF,
item_type_to_data_category(ty), 1);
discard_envelope_item(
item, SENTRY_DISCARD_REASON_RATELIMIT_BACKOFF);
continue;
}
}
Expand Down Expand Up @@ -1349,6 +1369,10 @@ data_category_to_string(sentry_data_category_t category)
return "trace_metric";
case SENTRY_DATA_CATEGORY_REPLAY:
return "replay";
case SENTRY_DATA_CATEGORY_LOG_BYTE:
return "log_byte";
case SENTRY_DATA_CATEGORY_TRACE_METRIC_BYTE:
return "trace_metric_byte";
case SENTRY_DATA_CATEGORY_MAX:
default:
return "unknown";
Expand Down Expand Up @@ -1425,10 +1449,7 @@ sentry__envelope_discard(const sentry_envelope_t *envelope,
if (rl && sentry__rate_limiter_is_disabled(rl, category)) {
continue;
}
const char *ty = sentry_value_as_string(
sentry_value_get_by_key(item->headers, "type"));
sentry__client_report_discard(
reason, item_type_to_data_category(ty), 1);
discard_envelope_item(item, reason);
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/sentry_logs.c
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,17 @@ send_log(sentry_level_t level, sentry_value_t log)
bool discarded = false;
SENTRY_WITH_OPTIONS (options) {
if (options->before_send_log_func) {
// the hook takes ownership and decrefs the log when it discards it,
// so we have to size it up front for the `log_byte` client report
const size_t log_bytes
= sentry__value_estimate_serialized_size(log);
log = options->before_send_log_func(
log, options->before_send_log_data);
if (sentry_value_is_null(log)) {
SENTRY_DEBUG("log was discarded by the `before_send_log` hook");
sentry__client_report_discard(SENTRY_DISCARD_REASON_BEFORE_SEND,
SENTRY_DATA_CATEGORY_LOG_ITEM, 1);
sentry__client_report_discard_with_bytes(
SENTRY_DISCARD_REASON_BEFORE_SEND,
SENTRY_DATA_CATEGORY_LOG_ITEM, 1, (long)log_bytes);
discarded = true;
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/sentry_metrics.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,20 @@ sentry_scope_capture_metric(sentry_scope_t *scope, sentry_metric_type_t type,
sentry__scope_free_one_shot(scope);
SENTRY_WITH_OPTIONS (options) {
if (options->before_send_metric_func) {
// the hook takes ownership and decrefs the metric when it
// discards it, so we have to size it up front for the
// `trace_metric_byte` client report
const size_t metric_bytes
= sentry__value_estimate_serialized_size(metric);
metric = options->before_send_metric_func(
metric, options->before_send_metric_data);
if (sentry_value_is_null(metric)) {
SENTRY_DEBUG("metric was discarded by the "
"`before_send_metric` hook");
sentry__client_report_discard(
sentry__client_report_discard_with_bytes(
SENTRY_DISCARD_REASON_BEFORE_SEND,
SENTRY_DATA_CATEGORY_TRACE_METRIC, 1);
SENTRY_DATA_CATEGORY_TRACE_METRIC, 1,
(long)metric_bytes);
discarded = true;
}
}
Expand Down
39 changes: 39 additions & 0 deletions src/sentry_value.c
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,45 @@ sentry__value_foreach_key_value(sentry_value_t value,
}
}

size_t
sentry__value_estimate_serialized_size(sentry_value_t value)
{
const thing_t *thing = value_as_thing(value);
if (!thing) {
if ((value._bits & TAG_MASK) == TAG_INT32) {
return 11; // upper bound, as in `-2147483648`
}
return value._bits == CONST_FALSE ? 5 : 4; // `false`, `true`, `null`
}
switch (thing_get_type(thing)) {
case THING_TYPE_LIST: {
const list_t *l = thing->payload._ptr;
size_t size = 2; // []
for (size_t i = 0; i < l->len; i++) {
size += sentry__value_estimate_serialized_size(l->items[i]);
}
return l->len ? size + l->len - 1 : size; // separating commas
}
case THING_TYPE_OBJECT: {
const obj_t *o = thing->payload._ptr;
size_t size = 2; // {}
for (size_t i = 0; i < o->len; i++) {
size += strlen(o->pairs[i].k) + 3; // "key":
size += sentry__value_estimate_serialized_size(o->pairs[i].v);
}
return o->len ? size + o->len - 1 : size; // separating commas
}
case THING_TYPE_STRING:
return strlen(thing->payload._ptr) + 2; // surrounding quotes
case THING_TYPE_DOUBLE:
case THING_TYPE_INT64:
case THING_TYPE_UINT64:
default:
// upper bound for any 64-bit integer or double rendering
return 24;
}
}

Comment on lines +1161 to +1199

@JoshuaMoelans JoshuaMoelans Jul 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Q: I'm a bit afraid of the overhead on this being called for every single log item, especially the recursive nature of the function... I have no insights into what size logs people are usually sending/discarding, so will need to do some benchmarking before committing to this...

any other approaches to keep track of the size are welcome; we could maybe store these things upon creating the sentry_value_t types and adding to the objects, but this is a larger rework of our internals so worth discussing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(potentially we make this PR just fix the log count numbers, and do the rework with log_bytes counting separately)

sentry_value_t
sentry_value_get_by_index_owned(sentry_value_t value, size_t index)
{
Expand Down
10 changes: 10 additions & 0 deletions src/sentry_value.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ sentry_value_t sentry__value_new_list_with_size(size_t size);
*/
sentry_value_t sentry__value_new_object_with_size(size_t size);

/**
* Estimates the size in bytes of `value`s JSON representation.
*
* This walks the value without allocating and does not account for string
* escaping or the exact rendering of numbers, so the result is an
* approximation. It is used for the `*_byte` client report categories, which
* explicitly allow one.
*/
size_t sentry__value_estimate_serialized_size(sentry_value_t value);

/**
* Iterates over the key/value pairs of an object value. The callback receives a
* borrowed reference for each value. Does nothing if `value` is not an object.
Expand Down
12 changes: 10 additions & 2 deletions tests/test_integration_client_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,14 @@ def test_client_report_before_send_log(cmake, httpserver):
envelope = Envelope.deserialize(httpserver.log[0][0].get_data())

assert_event(envelope)
# the discarded log is reported both as an item count and as a byte size,
# see https://develop.sentry.dev/sdk/telemetry/client-reports/#log-byte-outcomes
assert_client_report(
envelope,
[{"reason": "before_send", "category": "log_item", "quantity": 1}],
[
{"reason": "before_send", "category": "log_item", "quantity": 1},
{"reason": "before_send", "category": "log_byte"},
],
)


Expand Down Expand Up @@ -226,7 +231,10 @@ def test_client_report_before_send_metric(cmake, httpserver):
assert_event(envelope)
assert_client_report(
envelope,
[{"reason": "before_send", "category": "trace_metric", "quantity": 1}],
[
{"reason": "before_send", "category": "trace_metric", "quantity": 1},
{"reason": "before_send", "category": "trace_metric_byte"},
],
)


Expand Down
Loading
Loading