Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f2388a7
new content type + header fields + base test
tobias-wilfert Nov 11, 2025
1775126
remove useless comment + rename content-type
tobias-wilfert Nov 11, 2025
ffb40bc
add span_attachments to serialized spans
tobias-wilfert Nov 11, 2025
68b42dd
add outline for span attachment validation
tobias-wilfert Nov 11, 2025
fe40196
commit missing imports
tobias-wilfert Nov 11, 2025
85025db
add fast path dropping logic
tobias-wilfert Nov 12, 2025
0471c5c
wip
tobias-wilfert Nov 17, 2025
afb9e4e
add attachment_item to envelope quantities
tobias-wilfert Nov 20, 2025
d7fced5
address feedback + add serialize logic
tobias-wilfert Nov 20, 2025
3d0c531
make reject consistent with quantity
tobias-wilfert Nov 20, 2025
a397308
add more test + fix logic
tobias-wilfert Nov 20, 2025
cd99ad4
Merge branch 'master' into tobias-wilfert/feat/span-attachment
tobias-wilfert Nov 20, 2025
a44ddce
manual merge fix up
tobias-wilfert Nov 20, 2025
9f0e045
fix index_span
tobias-wilfert Nov 20, 2025
41eb6e7
update rate-limiting logic
tobias-wilfert Nov 24, 2025
c16e195
undo python change
tobias-wilfert Nov 24, 2025
9bb7098
remove superfluous comments
tobias-wilfert Nov 24, 2025
e3820f5
add test for the ratelimiting logic
tobias-wilfert Nov 24, 2025
fe815f6
make `AttachmentV2Meta` consistent with dev docs
tobias-wilfert Nov 24, 2025
9526118
Apply suggestions from code review
tobias-wilfert Nov 25, 2025
7c78b49
addressing feedback
tobias-wilfert Nov 25, 2025
41ddd91
Update relay-server/src/processing/spans/mod.rs
tobias-wilfert Nov 25, 2025
069aeea
addressing feedback (still missing quantity rework)
tobias-wilfert Nov 25, 2025
ca6ad29
quantity rework
tobias-wilfert Nov 25, 2025
8588f05
Merge branch 'master' into tobias-wilfert/feat/span-attachment
tobias-wilfert Nov 25, 2025
e01d6d0
Merge branch 'master' into tobias-wilfert/feat/span-attachment
tobias-wilfert Nov 26, 2025
d2c0fbe
fix existing integration tests
tobias-wilfert Nov 26, 2025
73af70b
final quantities update
tobias-wilfert Nov 26, 2025
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
3 changes: 3 additions & 0 deletions relay-dynamic-config/src/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ pub enum Feature {
#[doc(hidden)]
#[serde(rename = "organizations:indexed-spans-extraction")]
DeprecatedExtractSpansFromEvent,
/// Enable the experimental Span Attachment subset of the Span V2 processing pipeline in Relay.
#[serde(rename = "projects:span-v2-attachment-processing")]
SpanV2AttachmentProcessing,
/// Forward compatibility.
#[doc(hidden)]
#[serde(other)]
Expand Down
34 changes: 34 additions & 0 deletions relay-event-schema/src/protocol/attachment_v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};

use crate::processor::ProcessValue;
use crate::protocol::{Attributes, Timestamp};

use uuid::Uuid;

/// Metadata for a span attachment.
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
pub struct AttachmentV2Meta {
/// Unique identifier for this attachment.
#[metastructure(required = true, nonempty = true, trim = false)]
pub attachment_id: Annotated<Uuid>,

/// Timestamp when the attachment was created.
#[metastructure(required = true, trim = false)]
pub timestamp: Annotated<Timestamp>,

/// Original filename of the attachment.
#[metastructure(pii = "true", max_chars = 256, max_chars_allowance = 40, trim = false)]
pub filename: Annotated<String>,

/// Content type of the attachment body.
#[metastructure(required = true, max_chars = 128, trim = false)]
pub content_type: Annotated<String>,

/// Arbitrary attributes on a span attachment.
#[metastructure(pii = "maybe")]
pub attributes: Annotated<Attributes>,

/// Additional arbitrary fields for forwards compatibility.
#[metastructure(additional_properties, pii = "maybe")]
pub other: Object<Value>,
}
2 changes: 2 additions & 0 deletions relay-event-schema/src/protocol/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Implements the sentry event protocol.

mod attachment_v2;
mod attributes;
mod base;
mod breadcrumb;
Expand Down Expand Up @@ -41,6 +42,7 @@ mod utils;
#[doc(inline)]
pub use relay_base_schema::{events::*, spans::*};

pub use self::attachment_v2::*;
pub use self::attributes::*;
pub use self::breadcrumb::*;
pub use self::breakdowns::*;
Expand Down
5 changes: 5 additions & 0 deletions relay-server/src/envelope/content_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub enum ContentType {
SpanV2Container,
/// `application/vnd.sentry.items.trace-metric+json`
TraceMetricContainer,
/// `application/vnd.sentry.attachment.v2`
AttachmentV2,
/// All integration content types.
Integration(Integration),
/// Any arbitrary content type not listed explicitly.
Expand All @@ -57,6 +59,7 @@ impl ContentType {
Self::LogContainer => "application/vnd.sentry.items.log+json",
Self::SpanV2Container => "application/vnd.sentry.items.span.v2+json",
Self::TraceMetricContainer => "application/vnd.sentry.items.trace-metric+json",
Self::AttachmentV2 => "application/vnd.sentry.attachment.v2",
Self::Integration(integration) => integration.as_content_type(),
Self::Other(other) => other,
}
Expand Down Expand Up @@ -99,6 +102,8 @@ impl ContentType {
Some(Self::SpanV2Container)
} else if ct.eq_ignore_ascii_case(Self::TraceMetricContainer.as_str()) {
Some(Self::TraceMetricContainer)
} else if ct.eq_ignore_ascii_case(Self::AttachmentV2.as_str()) {
Some(Self::AttachmentV2)
} else {
Integration::from_content_type(ct).map(Self::Integration)
}
Expand Down
52 changes: 51 additions & 1 deletion relay-server/src/envelope/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::ops::AddAssign;
use uuid::Uuid;

use bytes::Bytes;
use relay_event_schema::protocol::EventType;
use relay_event_schema::protocol::{EventType, SpanId};
use relay_protocol::Value;
use relay_quotas::DataCategory;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -45,6 +45,8 @@ impl Item {
fully_normalized: false,
profile_type: None,
platform: None,
parent_id: None,
meta_length: None,
},
payload: Bytes::new(),
}
Expand Down Expand Up @@ -434,6 +436,30 @@ impl Item {
self.headers.sampled = sampled;
}

/// Returns the length of the item.
pub fn meta_length(&self) -> Option<u32> {
self.headers.meta_length
}

/// Sets the length of the optional meta segment.
///
/// Only applicable if the item is an attachment.
pub fn set_meta_length(&mut self, meta_length: u32) {
self.headers.meta_length = Some(meta_length);
}

/// Returns the parent entity that this item is associated with, if any.
///
/// Only applicable if the item is an attachment.
pub fn parent_id(&self) -> Option<&ParentId> {
self.headers.parent_id.as_ref()
}

/// Sets the parent entity that this item is associated with.
pub fn set_parent_id(&mut self, parent_id: ParentId) {
self.headers.parent_id = Some(parent_id);
}

/// Returns the specified header value, if present.
pub fn get_header<K>(&self, name: &K) -> Option<&Value>
where
Expand Down Expand Up @@ -947,6 +973,18 @@ pub struct ItemHeaders {
#[serde(default, skip)]
profile_type: Option<ProfileType>,

/// Content length of an optional meta segment that might be contained in the item.
///
/// For the time being such an meta segment is only present for span attachments.
#[serde(skip_serializing_if = "Option::is_none")]
meta_length: Option<u32>,

/// Parent entity that this item is associated with, if any.
///
/// For the time being only applicable if the item is a span-attachment.
#[serde(flatten, skip_serializing_if = "Option::is_none")]
parent_id: Option<ParentId>,

/// Other attributes for forward compatibility.
#[serde(flatten)]
other: BTreeMap<String, Value>,
Expand Down Expand Up @@ -994,6 +1032,18 @@ fn is_true(value: &bool) -> bool {
*value
}

/// Parent identifier for an attachment-v2.
///
/// Attachments can be associated with different types of parent entities (only spans for now).
///
/// SpanId(None) indicates that the item is a span-attachment that is associated with no specific
/// span.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ParentId {
SpanId(Option<SpanId>),
}

#[cfg(test)]
mod tests {
use crate::integrations::OtelFormat;
Expand Down
4 changes: 4 additions & 0 deletions relay-server/src/managed/counted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ impl Counted for Box<Envelope> {

let data = [
(DataCategory::Attachment, summary.attachment_quantity),
(
DataCategory::AttachmentItem,
summary.attachment_item_quantity,
),
(DataCategory::Profile, summary.profile_quantity),
(DataCategory::ProfileIndexed, summary.profile_quantity),
(DataCategory::Span, summary.span_quantity),
Expand Down
8 changes: 8 additions & 0 deletions relay-server/src/managed/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,14 @@ impl ManagedEnvelope {
);
}

if self.context.summary.attachment_item_quantity > 0 {
self.track_outcome(
outcome.clone(),
DataCategory::AttachmentItem,
self.context.summary.attachment_item_quantity,
);
}

if self.context.summary.monitor_quantity > 0 {
self.track_outcome(
outcome.clone(),
Expand Down
22 changes: 20 additions & 2 deletions relay-server/src/processing/spans/dynamic_sampling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pub fn validate_dsc(spans: &ExpandedSpans) -> Result<()> {
};

for span in &spans.spans {
let span = &span.span;
let trace_id = get_value!(span.trace_id);

if trace_id != Some(&dsc.trace_id) {
Expand Down Expand Up @@ -289,17 +290,34 @@ fn create_metrics(
/// as the total category is counted from now in in metrics.
struct UnsampledSpans {
spans: Vec<Item>,
attachments: Vec<Item>,
}

impl From<SerializedSpans> for UnsampledSpans {
fn from(value: SerializedSpans) -> Self {
Self { spans: value.spans }
Self {
spans: value.spans,
attachments: value.attachments,
}
Copy link

Choose a reason for hiding this comment

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

Bug: Legacy and integration spans dropped without outcomes tracking

When converting SerializedSpans to UnsampledSpans during dynamic sampling rejection, the legacy and integrations span fields are silently dropped without being included in the outcome tracking. Only spans and attachments are preserved in the conversion. This causes legacy span items and integration-sourced spans to be discarded without emitting proper outcomes, preventing clients from knowing these items were dropped.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting spot this used to be Self { spans: value.spans } so maybe this was always alread an issue 🤔

}
}

impl Counted for UnsampledSpans {
fn quantities(&self) -> Quantities {
let quantity = outcome_count(&self.spans) as usize;
smallvec::smallvec![(DataCategory::SpanIndexed, quantity),]
let mut quantities = smallvec::smallvec![];

if quantity > 0 {
quantities.push((DataCategory::SpanIndexed, quantity));
}
if !self.attachments.is_empty() {
quantities.push((
DataCategory::Attachment,
self.attachments.iter().map(Item::len).sum(),
));
quantities.push((DataCategory::AttachmentItem, self.attachments.len()));
}

quantities
}
}
18 changes: 16 additions & 2 deletions relay-server/src/processing/spans/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use relay_protocol::Annotated;
use crate::extractors::RequestMeta;
use crate::managed::Managed;
use crate::processing::Context;
use crate::processing::spans::{Error, ExpandedSpans, Result};
use crate::processing::spans::{Error, ExpandedSpans, Result, SerializedSpans};

/// Filters standalone spans sent for a project which does not allow standalone span ingestion.
pub fn feature_flag(ctx: Context<'_>) -> Result<()> {
Expand All @@ -15,11 +15,25 @@ pub fn feature_flag(ctx: Context<'_>) -> Result<()> {
}
}

// Filters span attachments for a project which does not allow for span attachment ingestion.
pub fn feature_flag_attachment(
spans: Managed<SerializedSpans>,
ctx: Context<'_>,
) -> Managed<SerializedSpans> {
spans.map(|mut spans, r| {
if ctx.should_filter(Feature::SpanV2AttachmentProcessing) {
let attachments = std::mem::take(&mut spans.attachments);
r.reject_err(Error::FilterFeatureFlag, attachments);
}
spans
})
}

/// Applies inbound filters to individual spans.
pub fn filter(spans: &mut Managed<ExpandedSpans>, ctx: Context<'_>) {
spans.retain_with_context(
|spans| (&mut spans.spans, spans.headers.meta()),
|span, meta, _| filter_span(span, meta, ctx),
|span, meta, _| filter_span(&span.span, meta, ctx),
);
}

Expand Down
Loading
Loading