From c112677df94079c1c37c4d4c82389f11ea1dbe9f Mon Sep 17 00:00:00 2001 From: David Herberth Date: Mon, 16 Feb 2026 14:26:45 +0100 Subject: [PATCH 1/2] feat(eap): Implements option to emit outcomes via EAP --- Cargo.lock | 8 +- Cargo.toml | 4 +- pyproject.toml | 2 +- relay-base-schema/src/data_category.rs | 17 +++- relay-dynamic-config/src/global.rs | 10 ++ relay-server/src/managed/counted.rs | 26 ++++++ relay-server/src/processing/logs/store.rs | 10 +- .../src/processing/trace_attachments/store.rs | 10 +- .../src/processing/trace_metrics/store.rs | 9 +- relay-server/src/processing/utils/store.rs | 21 ++++- relay-server/src/services/store.rs | 30 +++--- relay-server/src/services/store/sessions.rs | 1 + relay-server/src/services/upload.rs | 13 +-- tests/integration/test_ourlogs.py | 93 ++++++++++++++----- tests/integration/test_sessions_eap.py | 4 +- uv.lock | 6 +- 16 files changed, 188 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdf3814eec0..a14c05d062d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5074,9 +5074,9 @@ dependencies = [ [[package]] name = "sentry-kafka-schemas" -version = "2.1.21" +version = "2.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d151a1fd11abf859f75d199a49ae16c4e0460d4461171ae2c6a477599ca6d7" +checksum = "e9e5223681aafffe1d97fe54ce0808e2ea642bea0ef1ce568ed966a03e3193ff" dependencies = [ "jsonschema", "prost 0.14.3", @@ -5166,9 +5166,9 @@ dependencies = [ [[package]] name = "sentry_protos" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4544fc955b4587df17166521e836334c337c994d839f07e906d2838274d0d90a" +checksum = "dcca2ce7135d2721bd635ff5a55ce8f3b124f854a643a1f8dfc82410b04487df" dependencies = [ "prost 0.14.3", "prost-types", diff --git a/Cargo.toml b/Cargo.toml index 0defc423a78..3cf134b5fe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,12 +183,12 @@ sentry = { version = "0.41.0", default-features = false, features = [ "transport", ] } sentry-core = "0.41.0" -sentry-kafka-schemas = { version = "2.1.14", default-features = false } +sentry-kafka-schemas = { version = "2.1.23", default-features = false } sentry-release-parser = { version = "1.4.0", default-features = false, features = [ "semver-1", ] } sentry-types = "0.41.0" -sentry_protos = "0.5.0" +sentry_protos = "0.7.0" serde = { version = "=1.0.228", features = ["derive", "rc"] } serde-transcode = "1.1.1" serde-vars = "0.3.1" diff --git a/pyproject.toml b/pyproject.toml index 76b8cd24fa1..5342dc4bb10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dev = [ "pyyaml>=6.0.3", "redis>=5.0.1", "requests>=2.32.5", - "sentry-protos>=0.4.14", + "sentry-protos>=0.7.0", "sentry-sdk>=2.50.0", "types-protobuf>=6.30.2.20250703", "types-pyyaml>=6.0.12.20241230", diff --git a/relay-base-schema/src/data_category.rs b/relay-base-schema/src/data_category.rs index 7ccf7a068f7..8b2651f2bfe 100644 --- a/relay-base-schema/src/data_category.rs +++ b/relay-base-schema/src/data_category.rs @@ -10,7 +10,7 @@ use crate::events::EventType; /// An error that occurs if a number cannot be converted into a [`DataCategory`]. #[derive(Debug, PartialEq, thiserror::Error)] #[error("Unknown numeric data category {0} can not be converted into a DataCategory.")] -pub struct UnknownDataCategory(pub u8); +pub struct UnknownDataCategory(pub u32); /// Classifies the type of data that is being ingested. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -420,11 +420,20 @@ impl TryFrom for DataCategory { 32 => Ok(Self::InstallableBuild), 33 => Ok(Self::TraceMetric), 34 => Ok(Self::SeerUser), - other => Err(UnknownDataCategory(other)), + other => Err(UnknownDataCategory(other as u32)), } } } +impl TryFrom for DataCategory { + type Error = UnknownDataCategory; + + fn try_from(value: u32) -> Result { + let value = u8::try_from(value).map_err(|_| UnknownDataCategory(value))?; + value.try_into() + } +} + /// The unit in which a data category is measured. /// /// This enum specifies how quantities for different data categories are measured, @@ -543,8 +552,8 @@ mod tests { // If this test fails, update the numeric bounds so that the first assertion // maps to the last variant in the enum and the second assertion produces an error // that the DataCategory does not exist. - assert_eq!(DataCategory::try_from(34), Ok(DataCategory::SeerUser)); - assert_eq!(DataCategory::try_from(35), Err(UnknownDataCategory(35))); + assert_eq!(DataCategory::try_from(34u8), Ok(DataCategory::SeerUser)); + assert_eq!(DataCategory::try_from(35u8), Err(UnknownDataCategory(35))); } #[test] diff --git a/relay-dynamic-config/src/global.rs b/relay-dynamic-config/src/global.rs index 3748e3bcea7..5acb52ef0e7 100644 --- a/relay-dynamic-config/src/global.rs +++ b/relay-dynamic-config/src/global.rs @@ -181,6 +181,16 @@ pub struct Options { )] pub sessions_eap_rollout_rate: f32, + /// Rollout rate for accepted outcomes being emitted by EAP instead of Relay. + /// + /// Rate needs to be between `0.0` and `1.0`. + #[serde( + rename = "relay.eap-outcomes.rollout-rate", + deserialize_with = "default_on_error", + skip_serializing_if = "is_default" + )] + pub eap_outcomes_rollout_rate: f32, + /// All other unknown options. #[serde(flatten)] other: HashMap, diff --git a/relay-server/src/managed/counted.rs b/relay-server/src/managed/counted.rs index dbe85f0bbd8..9c25b56eba6 100644 --- a/relay-server/src/managed/counted.rs +++ b/relay-server/src/managed/counted.rs @@ -193,6 +193,32 @@ impl Counted for SessionAggregateItem { } } +#[cfg(feature = "processing")] +impl Counted for sentry_protos::snuba::v1::Outcomes { + fn quantities(&self) -> Quantities { + self.category_count + .iter() + .inspect(|cc| { + debug_assert!(DataCategory::try_from(cc.data_category).is_ok()); + debug_assert!(usize::try_from(cc.quantity).is_ok()); + }) + .filter_map(|cc| { + Some(( + DataCategory::try_from(cc.data_category).ok()?, + usize::try_from(cc.quantity).ok()?, + )) + }) + .collect() + } +} + +#[cfg(feature = "processing")] +impl Counted for sentry_protos::snuba::v1::TraceItem { + fn quantities(&self) -> Quantities { + self.outcomes.quantities() + } +} + impl Counted for &T where T: Counted, diff --git a/relay-server/src/processing/logs/store.rs b/relay-server/src/processing/logs/store.rs index ccbae873627..5a4cf379b5d 100644 --- a/relay-server/src/processing/logs/store.rs +++ b/relay-server/src/processing/logs/store.rs @@ -9,7 +9,9 @@ use uuid::Uuid; use crate::envelope::WithHeader; use crate::processing::logs::{Error, Result}; -use crate::processing::utils::store::{extract_meta_attributes, proto_timestamp, uuid_to_item_id}; +use crate::processing::utils::store::{ + extract_meta_attributes, proto_timestamp, quantities_to_trace_item_outcomes, uuid_to_item_id, +}; use crate::processing::{self, Counted, Retention}; use crate::services::outcome::DiscardReason; use crate::services::store::StoreTraceItem; @@ -74,12 +76,10 @@ pub fn convert(log: WithHeader, ctx: &Context) -> Result attributes: attributes(meta, attrs, fields), client_sample_rate: 1.0, server_sample_rate: 1.0, + outcomes: Some(quantities_to_trace_item_outcomes(quantities, ctx.scoping)), }; - Ok(StoreTraceItem { - trace_item, - quantities, - }) + Ok(StoreTraceItem { trace_item }) } /// Fields on the log message which are stored as fields. diff --git a/relay-server/src/processing/trace_attachments/store.rs b/relay-server/src/processing/trace_attachments/store.rs index daf1b5270e0..e99d9a49679 100644 --- a/relay-server/src/processing/trace_attachments/store.rs +++ b/relay-server/src/processing/trace_attachments/store.rs @@ -6,12 +6,12 @@ use relay_protocol::{Annotated, IntoValue, Value}; use relay_quotas::Scoping; use sentry_protos::snuba::v1::{AnyValue, TraceItem, TraceItemType, any_value}; -use crate::managed::{Managed, Rejected}; +use crate::managed::{Counted, Managed, Quantities, Rejected}; use crate::processing::Retention; use crate::processing::trace_attachments::types::ExpandedAttachment; use crate::processing::utils::store::{ AttributeMeta, extract_client_sample_rate, extract_meta_attributes, proto_timestamp, - uuid_to_item_id, + quantities_to_trace_item_outcomes, uuid_to_item_id, }; use crate::services::outcome::{DiscardReason, Outcome}; use crate::services::upload::StoreAttachment; @@ -25,6 +25,8 @@ pub fn convert( let scoping = attachment.scoping(); let received_at = attachment.received_at(); attachment.try_map(|attachment, _record_keeper| { + let quantities = attachment.quantities(); + let ExpandedAttachment { parent_id, meta, @@ -37,7 +39,7 @@ pub fn convert( retention, server_sample_rate, }; - let trace_item = attachment_to_trace_item(meta, ctx) + let trace_item = attachment_to_trace_item(meta, quantities, ctx) .ok_or(Outcome::Invalid(DiscardReason::InvalidTraceAttachment))?; Ok::<_, Outcome>(StoreAttachment { trace_item, body }) @@ -62,6 +64,7 @@ struct Context { fn attachment_to_trace_item( meta: Annotated, + quantities: Quantities, ctx: Context, ) -> Option { let meta = meta.into_value()?; @@ -99,6 +102,7 @@ fn attachment_to_trace_item( retention_days: ctx.retention.standard as u32, received: Some(proto_timestamp(ctx.received_at)), downsampled_retention_days: ctx.retention.downsampled as u32, + outcomes: Some(quantities_to_trace_item_outcomes(quantities, ctx.scoping)), }; Some(trace_item) } diff --git a/relay-server/src/processing/trace_metrics/store.rs b/relay-server/src/processing/trace_metrics/store.rs index 15c1a8b9c3c..1f356cb6433 100644 --- a/relay-server/src/processing/trace_metrics/store.rs +++ b/relay-server/src/processing/trace_metrics/store.rs @@ -12,7 +12,8 @@ use uuid::Uuid; use crate::envelope::WithHeader; use crate::processing::trace_metrics::{Error, Result}; use crate::processing::utils::store::{ - extract_client_sample_rate, extract_meta_attributes, uuid_to_item_id, + extract_client_sample_rate, extract_meta_attributes, quantities_to_trace_item_outcomes, + uuid_to_item_id, }; use crate::processing::{self, Counted, Retention}; use crate::services::outcome::DiscardReason; @@ -75,12 +76,10 @@ pub fn convert(metric: WithHeader, ctx: &Context) -> Result) -> Timestamp { diff --git a/relay-server/src/processing/utils/store.rs b/relay-server/src/processing/utils/store.rs index bca56e92276..3a73872ab2e 100644 --- a/relay-server/src/processing/utils/store.rs +++ b/relay-server/src/processing/utils/store.rs @@ -6,10 +6,13 @@ use relay_conventions::CLIENT_SAMPLE_RATE; use relay_event_schema::protocol::Attributes; use relay_protocol::{Annotated, IntoValue, MetaTree, Value}; -use sentry_protos::snuba::v1::{AnyValue, ArrayValue, any_value}; +use relay_quotas::Scoping; +use sentry_protos::snuba::v1::{AnyValue, ArrayValue, CategoryCount, Outcomes, any_value}; use serde::Serialize; use uuid::Uuid; +use crate::managed::Quantities; + /// Represents metadata extracted from Relay's annotated model. /// /// This struct holds metadata about processing errors, transformations, and other @@ -221,3 +224,19 @@ pub fn item_id_to_uuid(item_id: &[u8]) -> Result { let item_id = u128::from_le_bytes(item_id); Ok(Uuid::from_u128(item_id)) } + +/// Converts [`Quantities`] and [`Scoping`] into Trace Item [`Outcomes`]. +pub fn quantities_to_trace_item_outcomes(q: Quantities, scoping: Scoping) -> Outcomes { + let category_count = q + .into_iter() + .map(|(category, quantity)| CategoryCount { + data_category: category as u32, + quantity: quantity as u64, + }) + .collect(); + + Outcomes { + category_count, + key_id: scoping.key_id.unwrap_or(0), + } +} diff --git a/relay-server/src/services/store.rs b/relay-server/src/services/store.rs index 0ec49f1df03..5a586123cb3 100644 --- a/relay-server/src/services/store.rs +++ b/relay-server/src/services/store.rs @@ -111,16 +111,11 @@ pub struct StoreMetrics { pub struct StoreTraceItem { /// The final trace item which will be produced to Kafka. pub trace_item: TraceItem, - /// Outcomes to be emitted when successfully producing the item to Kafka. - /// - /// Note: this is only a temporary measure, long term these outcomes will be part of the trace - /// item and emitted by Snuba to guarantee a delivery to storage. - pub quantities: Quantities, } impl Counted for StoreTraceItem { fn quantities(&self) -> Quantities { - self.quantities.clone() + self.trace_item.quantities() } } @@ -631,18 +626,31 @@ impl StoreService { let scoping = message.scoping(); let received_at = message.received_at(); - let quantities = message.try_accept(|item| { + let eap_emits_outcomes = utils::is_rolled_out( + scoping.organization_id.value(), + self.global_config + .current() + .options + .eap_outcomes_rollout_rate, + ) + .is_keep(); + + let outcomes = message.try_accept(|mut item| { + let outcomes = match eap_emits_outcomes { + true => None, + false => item.trace_item.outcomes.take(), + }; + let message = KafkaMessage::for_item(scoping, item.trace_item); - self.produce(KafkaTopic::Items, message) - .map(|()| item.quantities) + self.produce(KafkaTopic::Items, message).map(|()| outcomes) }); // Accepted outcomes when items have been successfully produced to rdkafka. // // This is only a temporary measure, long term these outcomes will be part of the trace // item and emitted by Snuba to guarantee a delivery to storage. - if let Ok(quantities) = quantities { - for (category, quantity) in quantities { + if let Ok(Some(outcomes)) = outcomes { + for (category, quantity) in outcomes.quantities() { self.outcome_aggregator.send(TrackOutcome { category, event_id: None, diff --git a/relay-server/src/services/store/sessions.rs b/relay-server/src/services/store/sessions.rs index f329c8788e4..234cfcf12fe 100644 --- a/relay-server/src/services/store/sessions.rs +++ b/relay-server/src/services/store/sessions.rs @@ -76,6 +76,7 @@ pub fn to_trace_item(scoping: Scoping, bucket: Bucket, retention: u16) -> Option attributes, client_sample_rate: 1.0, server_sample_rate: 1.0, + outcomes: None, }) } diff --git a/relay-server/src/services/upload.rs b/relay-server/src/services/upload.rs index 9ab823db982..b19949566f4 100644 --- a/relay-server/src/services/upload.rs +++ b/relay-server/src/services/upload.rs @@ -9,10 +9,8 @@ use futures::stream::FuturesUnordered; use futures::{FutureExt, StreamExt}; use objectstore_client::{Client, ExpirationPolicy, Session, Usecase}; use relay_config::UploadServiceConfig; -use relay_quotas::DataCategory; use relay_system::{Addr, FromMessage, Interface, NoResponse, Receiver, Service}; use sentry_protos::snuba::v1::TraceItem; -use smallvec::smallvec; use crate::constants::DEFAULT_ATTACHMENT_RETENTION; use crate::envelope::ItemType; @@ -81,10 +79,7 @@ pub struct StoreAttachment { impl Counted for StoreAttachment { fn quantities(&self) -> Quantities { - smallvec![ - (DataCategory::AttachmentItem, 1), - (DataCategory::Attachment, self.body.len()), - ] + self.trace_item.quantities() } } @@ -316,7 +311,6 @@ impl UploadServiceInner { .map_err(Error::UploadFailed) .reject(&managed)?; - let quantities = managed.quantities(); let body = Bytes::clone(&managed.body); // Make sure that the attachment can be converted into a trace item: @@ -325,10 +319,7 @@ impl UploadServiceInner { trace_item, body: _, } = attachment; - Ok::<_, Error>(StoreTraceItem { - trace_item, - quantities, - }) + Ok::<_, Error>(StoreTraceItem { trace_item }) })?; // Upload the attachment: diff --git a/tests/integration/test_ourlogs.py b/tests/integration/test_ourlogs.py index de343130c91..f1a2f7cd6bc 100644 --- a/tests/integration/test_ourlogs.py +++ b/tests/integration/test_ourlogs.py @@ -122,19 +122,20 @@ def test_ourlog_multiple_containers_not_allowed( ] +@pytest.mark.parametrize("eap_emits_outcomes", [True, False]) @pytest.mark.parametrize( - "external_mode,expected_byte_size", + "external_mode,expected_byte_size_1,expected_byte_size_2", [ # 296 here is a billing relevant metric, do not arbitrarily change it, # this value is supposed to be static and purely based on data received, # independent of any normalization. - (None, 377), + (None, 18, 359), # Same applies as above, a proxy Relay does not need to run normalization. - ("proxy", 377), + ("proxy", 18, 359), # If an external Relay/Client makes modifications, sizes can change, # this is fuzzy due to slight changes in sizes due to added timestamps # and may need to be adjusted when changing normalization. - ("managed", 587), + ("managed", 128, 459), ], ) def test_ourlog_extraction_with_sentry_logs( @@ -145,13 +146,18 @@ def test_ourlog_extraction_with_sentry_logs( items_consumer, outcomes_consumer, external_mode, - expected_byte_size, + expected_byte_size_1, + expected_byte_size_2, + eap_emits_outcomes, ): relay_fn = relay items_consumer = items_consumer() outcomes_consumer = outcomes_consumer() + if eap_emits_outcomes: + mini_sentry.global_config["options"]["relay.eap-outcomes.rollout-rate"] = 1.0 + project_id = 42 project_config = mini_sentry.add_full_project_config(project_id) project_config["config"]["retentions"] = { @@ -241,6 +247,25 @@ def test_ourlog_extraction_with_sentry_logs( ts, delta=timedelta(seconds=1), expect_resolution="ns" ), "traceId": "5b8efff798038103d269b633813fc60c", + **( + { + "outcomes": { + "categoryCount": [ + { + "dataCategory": DataCategory.LOG_ITEM.value, + "quantity": "1", + }, + { + "dataCategory": DataCategory.LOG_BYTE.value, + "quantity": f"{expected_byte_size_1}", + }, + ], + "keyId": "123", + } + } + if eap_emits_outcomes + else {} + ), }, { "attributes": { @@ -307,28 +332,48 @@ def test_ourlog_extraction_with_sentry_logs( ts, delta=timedelta(seconds=1), expect_resolution="ns" ), "traceId": "5b8efff798038103d269b633813fc60c", + **( + { + "outcomes": { + "categoryCount": [ + { + "dataCategory": DataCategory.LOG_ITEM.value, + "quantity": "1", + }, + { + "dataCategory": DataCategory.LOG_BYTE.value, + "quantity": f"{expected_byte_size_2}", + }, + ], + "keyId": "123", + } + } + if eap_emits_outcomes + else {} + ), }, ] - outcomes = outcomes_consumer.get_aggregated_outcomes(n=2) - assert outcomes == [ - { - "category": DataCategory.LOG_ITEM.value, - "key_id": 123, - "org_id": 1, - "outcome": 0, - "project_id": 42, - "quantity": 2, - }, - { - "category": DataCategory.LOG_BYTE.value, - "key_id": 123, - "org_id": 1, - "outcome": 0, - "project_id": 42, - "quantity": expected_byte_size, - }, - ] + if not eap_emits_outcomes: + outcomes = outcomes_consumer.get_aggregated_outcomes(n=2) + assert outcomes == [ + { + "category": DataCategory.LOG_ITEM.value, + "key_id": 123, + "org_id": 1, + "outcome": 0, + "project_id": 42, + "quantity": 2, + }, + { + "category": DataCategory.LOG_BYTE.value, + "key_id": 123, + "org_id": 1, + "outcome": 0, + "project_id": 42, + "quantity": expected_byte_size_1 + expected_byte_size_2, + }, + ] def test_ourlog_extraction_with_string_pii_scrubbing( diff --git a/tests/integration/test_sessions_eap.py b/tests/integration/test_sessions_eap.py index 4b4aff24223..f5e36ec7788 100644 --- a/tests/integration/test_sessions_eap.py +++ b/tests/integration/test_sessions_eap.py @@ -59,7 +59,7 @@ def test_session_eap_double_write( "projectId": "42", "traceId": mock.ANY, "itemId": mock.ANY, - "itemType": 12, + "itemType": "TRACE_ITEM_TYPE_USER_SESSION", "timestamp": time_within_delta(started, delta=timedelta(seconds=2)), "received": time_within_delta(), "retentionDays": 90, @@ -80,7 +80,7 @@ def test_session_eap_double_write( "projectId": "42", "traceId": mock.ANY, "itemId": mock.ANY, - "itemType": 12, + "itemType": "TRACE_ITEM_TYPE_USER_SESSION", "timestamp": time_within_delta(started, delta=timedelta(seconds=2)), "received": time_within_delta(), "retentionDays": 90, diff --git a/uv.lock b/uv.lock index 3f0556a9e97..8c13cb23ddc 100644 --- a/uv.lock +++ b/uv.lock @@ -572,7 +572,7 @@ dev = [ { name = "pyyaml", specifier = ">=6.0.3" }, { name = "redis", specifier = ">=5.0.1" }, { name = "requests", specifier = ">=2.32.5" }, - { name = "sentry-protos", specifier = ">=0.4.14" }, + { name = "sentry-protos", specifier = ">=0.7.0" }, { name = "sentry-sdk", specifier = ">=2.50.0" }, { name = "setuptools", specifier = "==80.9.0" }, { name = "types-protobuf", specifier = ">=6.30.2.20250703" }, @@ -613,7 +613,7 @@ wheels = [ [[package]] name = "sentry-protos" -version = "0.4.14" +version = "0.7.0" source = { registry = "https://pypi.devinfra.sentry.io/simple" } dependencies = [ { name = "grpc-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -621,7 +621,7 @@ dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] wheels = [ - { url = "https://pypi.devinfra.sentry.io/wheels/sentry_protos-0.4.14-py3-none-any.whl", hash = "sha256:b6e6802c49abc69427f947f5a4a8d6ade5dcf64862e34fc9325d4182a1b6f5c6" }, + { url = "https://pypi.devinfra.sentry.io/wheels/sentry_protos-0.7.0-py3-none-any.whl", hash = "sha256:08fd8c88b50c14c2b95b6f23ea0ea2b4afec1e82b49484a95c914d8daf94a2d5" }, ] [[package]] From 76fc3e25751aa0630e81ce4b8779a09d94c424e0 Mon Sep 17 00:00:00 2001 From: David Herberth Date: Wed, 18 Feb 2026 17:49:05 +0100 Subject: [PATCH 2/2] update outdated comments --- tests/integration/test_ourlogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_ourlogs.py b/tests/integration/test_ourlogs.py index f1a2f7cd6bc..d10c36a2a65 100644 --- a/tests/integration/test_ourlogs.py +++ b/tests/integration/test_ourlogs.py @@ -126,8 +126,8 @@ def test_ourlog_multiple_containers_not_allowed( @pytest.mark.parametrize( "external_mode,expected_byte_size_1,expected_byte_size_2", [ - # 296 here is a billing relevant metric, do not arbitrarily change it, - # this value is supposed to be static and purely based on data received, + # The values here are billing relevant metrics, do not arbitrarily change it, + # these values are supposed to be static and purely based on data received, # independent of any normalization. (None, 18, 359), # Same applies as above, a proxy Relay does not need to run normalization.