fix: stop double-reporting Segment transport errors and downgrade lossless retries to warnings - #9808
fix: stop double-reporting Segment transport errors and downgrade lossless retries to warnings#9808alejandro-jimenez-dcl wants to merge 1 commit into
Conversation
…sless retries to warnings
## Problem
Segment analytics transport errors bill the Sentry error budget twice per failure and
report genuinely-lossless retries at error level — a ~88 events/week family across 7 Sentry
groups, with a Sentry alert that keeps auto-minting duplicate GitHub issues from it.
## Root cause
Three reporting-path defects (scope corrected by adversarial review):
1. Channel duplication: the native `report_error` fans one failure into both C# callbacks —
`ErrorCallback` (descriptive message) and `Callback` (bare `Error` code) — and both
called `LogException`, so every failed operation was double-billed.
2. The send-loop retry (`"Error executing send loop (will retry)"`) is emitted when the
native daemon retries WITHOUT consuming the spooled item — genuinely lossless, so
Warning is the honest severity. A prior once-per-session latch band-aid (its own TODO:
remove once the core issue is solved) is superseded.
3. `ReportHub.LogWarning(ANALYTICS, ...)` was matrix-dead: no shipped
`ReportsHandlingSettings` asset enabled (ANALYTICS, Warning), so any downgrade would
have been total silence in real players.
Deliberately NOT reclassified (real data loss, kept at error level until the native fixes
land): instant-track `"Network error"` (event dropped, never spooled — lost revenue-funnel
events), `"database is locked"` flush failures (extracted batch dropped), `"(will drop)"`,
and `message too large`.
## Fix (~35 production LOC + 6 asset lines)
- `RustSegmentAnalyticsService`: `IsRetriedSendLoopError` classifier matching only the
provably-lossless retry pattern → `LogWarning`; everything else stays `LogException`; the
once-per-session latch is deleted. `Callback`'s not-Success report becomes a warning (the
paired `ErrorCallback` always carries the descriptive error-level report for the same
operation id).
- `ReportsHandlingSettings{Production,Development}.asset`: enable (ANALYTICS, Warning) in
the production sentry + debugLog matrices and the development debugLog matrix, pinned by
an asset-loading test.
Net: ~66 of ~88 events/week reclassified with zero signal loss; the lossy patterns keep
billing the error budget by design. Follow-ups for the owning team (native): instant-track
enqueue fallback, flush ordering, sqlite busy_timeout.
## Test
New EditMode `RustSegmentErrorReportingShould` (7 tests): every send-loop retry reported as
a warning (twice — proving the latch is gone), the duplicate operation-callback channel
downgraded, the shipped-matrix guard, and four `Keep...AsException` boundary tests pinning
the non-downgrade of the lossy/actionable patterns.
## Validation
Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 3/7 as intended (exception-level
logs where warnings are expected; matrices don't enable ANALYTICS warnings at pin; the 4
preservation guards pass at pin) / GREEN PASS 7/7.
Related: #8574 (open — P13 send-loop retries; stops accruing), #7906 (stale-closed JVW
tracker — NOT silenced: instant-track loss keeps error-level signal; its duplicate-channel
echo is removed), #7867 (closed — the duplicate-channel form). Intentionally unaffected:
#8218 (locked-flush batch loss) and #7866 (message-too-large) keep accruing until the
native fixes land.
Includes inspection-warning cleanup in all touched files.
|
Claude encountered an error after 9m 0s —— View job Review
Part 1 — Dependency / supply-chain reviewThe Step 1 — dependency changes identified
The only files touched are two Steps 1.5–3 (registry metadata, capability assessment, transitive risk) are not applicable — there is no new or changed dependency to look up. Step W — workflow / prompt files[PR] Nothing under Summary table
No MEDIUM or HIGH supply-chain items. DEPENDENCY_REVIEW: PASS Part 2 — Functional reviewThe core diagnosis is sound and I verified the load-bearing claims against the sources rather than taking the description at face value:
Findings1 · HIGH — the matrix flip is category-wide and un-mutes 18 unrelated, un-debounced sites · inline
I did not measure the production rate of those 18 sites, so the actual volume is UNKNOWN — but the PR reasons only about the two Segment call sites and doesn't account for the other 18 riding along. Scope the flip to a dedicated 2 · MEDIUM —
3 · MEDIUM — the classifier string is owned by an external crate · inline
4 · MEDIUM — reflection-bound tests where
5 · LOW — The tests rely on the default 6 · LOW — Correctly guarded with a per-run GUID fingerprint, but each run leaves a tracker in the shared static dictionary for up to the 5-minute 7 · LOW / pre-existing — the debouncer counter is shared across handlers
VerdictFindings 2–7 are refinements. Finding 1 is the one I'd want resolved before merge — as written, the PR trades ~66 error-level events/week for an unmeasured stream of un-debounced warnings across 18 unrelated call sites, on both the player log and the Sentry breadcrumb ring. Scoping the matrix change to a dedicated category keeps the entire benefit with none of that exposure. I did not run the EditMode test lane (no Unity in this environment) or the ReSharper lint hook — the PR's own Windows Unity 6000.4 validation is the reference for those. No files were changed. |
| - Category: ANALYTICS | ||
| Severity: 2 |
There was a problem hiding this comment.
Category-wide flip un-mutes 18 unrelated call sites. (ANALYTICS, Warning) is a whole-category cell, and the PR's own defect #3 correctly notes it was matrix-dead. Turning it on here doesn't just enable RustSegmentAnalyticsService — SentryTransactionManager.cs has 18 ReportHub.LogWarning(new ReportData(ReportCategory.ANALYTICS), …) sites, none of which carry a debouncer, and they now reach both the production player log and SentrySdk.AddBreadcrumb.
At least one path is per-web-request: SentryWebRequestHandler.OnRequestStarted early-returns for file:// URLs (SentryWebRequestHandler.cs:45) so no transaction is registered, but OnRequestFinished (:101 → StartSpan) and OnProcessDataFinished (:111-112 → EndCurrentSpan + EndTransaction) run unconditionally — three ANALYTICS warnings per such request.
Sentry's breadcrumb ring is bounded (default 100 per scope), so an un-debounced ANALYTICS warning stream evicts the breadcrumbs that make real crash reports diagnosable — inverting the PR's goal.
Suggested scoping: add a dedicated constant to ReportCategory.cs (e.g. ANALYTICS_TRANSPORT) for the Segment FFI reports and enable Warning only for that cell, leaving ANALYTICS/Warning off. Alternatively, state the measured production volume of those 18 sites so the trade is explicit.
| // The native send daemon retries a "(will retry)" send-loop failure without consuming the | ||
| // spooled item, so no events are lost and it must not bill the Sentry error budget. | ||
| // Every other native error is a potential loss signal and keeps exception-level reporting: | ||
| // instant_track_and_flush drops the event on send failure (bare "Network error", never | ||
| // spooled), a failed flush drops the already-extracted batch ("Cannot flush: ..."), and | ||
| // the "(will drop)" send-loop branch consumes the item. | ||
| private static bool IsRetriedSendLoopError(string message) => | ||
| message.Contains("Error executing send loop (will retry)"); |
There was a problem hiding this comment.
The matched literal is not produced by anything in this repo — it comes from AnalyticsEventSendDaemon in the segment crate (.native/Cargo.toml: git = "https://github.com/decentraland/segment", rev = "2d46f10f9a5da2feac6554daedfc17d1ba49952e"). Nothing couples this C# string to that rev, so a future crate bump can silently change the wording and the classifier stops matching.
It fails in the safe direction (back to error level, i.e. the current behaviour), so this isn't a blocker — but the comment should name the source so whoever bumps the rev has a breadcrumb:
| // The native send daemon retries a "(will retry)" send-loop failure without consuming the | |
| // spooled item, so no events are lost and it must not bill the Sentry error budget. | |
| // Every other native error is a potential loss signal and keeps exception-level reporting: | |
| // instant_track_and_flush drops the event on send failure (bare "Network error", never | |
| // spooled), a failed flush drops the already-extracted batch ("Cannot flush: ..."), and | |
| // the "(will drop)" send-loop branch consumes the item. | |
| private static bool IsRetriedSendLoopError(string message) => | |
| message.Contains("Error executing send loop (will retry)"); | |
| // The native send daemon retries a "(will retry)" send-loop failure without consuming the | |
| // spooled item, so no events are lost and it must not bill the Sentry error budget. | |
| // Every other native error is a potential loss signal and keeps exception-level reporting: | |
| // instant_track_and_flush drops the event on send failure (bare "Network error", never | |
| // spooled), a failed flush drops the already-extracted batch ("Cannot flush: ..."), and | |
| // the "(will drop)" send-loop branch consumes the item. | |
| // The matched text is emitted by AnalyticsEventSendDaemon in the `segment` crate pinned in | |
| // .native/Cargo.toml; re-check this literal when that rev is bumped. | |
| private static bool IsRetriedSendLoopError(string message) => | |
| message.Contains("Error executing send loop (will retry)"); |
| // carrying the descriptive message — severity classification lives there. | ||
| if (response is not NativeMethods.Response.Success) | ||
| ReportHub.LogException(new Exception($"Segment operation {operationId} {type} failed with: {response}"), ReportCategory.ANALYTICS); | ||
| ReportHub.LogWarning(ReportCategory.ANALYTICS, $"Segment operation {operationId} {type} failed with: {response}"); |
There was a problem hiding this comment.
The duplicate-channel premise checks out against the native source — AppContext::report_error (.native/src/server.rs:426-436) unconditionally calls error_fn(message) and then callback_fn(id, Response::Error), and report_success is the only other callback_fn caller. So every Response::Error that reaches here is guaranteed to have a paired descriptive ErrorCallback. Good analysis.
Two follow-ups on the replacement severity though:
ReportCategory.ANALYTICShere goes through the implicitstring → ReportDataconversion, soDebounceisNONE. Combined with the matrix change, every failed operation now unconditionally adds a Sentry breadcrumb. Debouncing wouldn't help anyway (the message embeds a uniqueoperationId) — which is really an argument that this report carries no information the pairedErrorCallbackdoesn't already carry.- Given (1),
LogType.Log(ReportHub.Log(...), already used three lines above for the success case) would be the honest level for a strictly-redundant report, and it wouldn't depend on the(ANALYTICS, Warning)matrix flip at all. Right now this line is the only thing in the PR that needs Warning enabled for the DebugLog matrix.
decentraland-bot
left a comment
There was a problem hiding this comment.
Jarvis Review — PR #9808
STEP 2 — Root-cause check
The PR correctly identifies three reporting-path defects:
- Channel duplication:
report_errorfans one failure into bothErrorCallback(descriptive message) andCallback(bareErrorcode), and both calledLogException→ double Sentry error-budget billing. - Lossless retry at error level: The
"(will retry)"send-loop error is emitted when the native daemon retries without consuming the spooled item — genuinely lossless, so Warning is the honest severity. - Matrix-dead Warning: No shipped
ReportsHandlingSettingsasset enabled(ANALYTICS, Warning), so any downgrade would have been silent.
Verified against native source: AppContext::report_error (.native/src/server.rs:426-436) unconditionally calls error_fn(message) and callback_fn(id, Response::Error), confirming the paired-callback premise.
The fix addresses root causes, not symptoms. The deliberate preservation of error-level for lossy patterns ("Network error", "database is locked", "(will drop)", message-too-large) is well-reasoned.
PASS ✅
STEP 3 — Design & integration
No new lifecycle units introduced. All changes are within the existing RustSegmentAnalyticsService:
RETRY_REPORT— static configuration for debouncing, not a lifecycle managerIsRetriedSendLoopError— pure stateless classifier- Nullable annotation cleanup on
CURRENT— type-correctness only
No ownership concerns, no reconciliation patterns, no persistent state outside ECS (the debouncer is infrastructure configuration). The ProgressiveWindowDebouncer is static and shared across the singleton service's lifetime — appropriate since only one RustSegmentAnalyticsService instance exists at a time (mutex-enforced).
The deletion of ONCE_PATTERN_ALREADY_CAUGHT is a strict improvement — the replacement (debounced warning with progressive windowing) is superior: it bounds volume while keeping distinct stuck items visible.
PASS ✅
STEP 4 — Member audit
No new public members. All additions are private static:
RETRY_REPORT— consumed only byErrorCallbackIsRetriedSendLoopError— consumed only byErrorCallback
The Callback method behavior change (LogException → LogWarning for non-Success) is internal to the native callback. No single-use accessor, absent≠false, or re-derivation issues.
PASS ✅
STEP 5 — Line-level review
Finding 1 — [P1] Category-wide matrix flip un-mutes 18 unrelated, un-debounced warning sites
Location: ReportsHandlingSettingsProduction.asset (both debugLog and sentry matrices) and ReportsHandlingSettingsDevelopment.asset
Enabling (ANALYTICS, Warning) in the production matrices also un-mutes 18 ReportHub.LogWarning(new ReportData(ReportCategory.ANALYTICS), …) calls in SentryTransactionManager.cs (lines 69, 80, 91, 102, 113, 124, 135, 212, 218, 233, 239, 258, 264, 293, 340, 379, 385, 428), none of which carry a debouncer.
Critically, SentryWebRequestHandler.OnRequestStarted early-returns for file:// URLs without registering a transaction, but OnRequestFinished → Instance.StartSpan() and OnProcessDataFinished → Instance.EndCurrentSpan() + Instance.EndTransaction() still execute. Each hits the "transaction not found" guard, producing 3 ANALYTICS warnings per such request. These now reach both the player log and Sentry breadcrumbs.
Sentry's breadcrumb ring is bounded (default 100 per scope), so an un-debounced ANALYTICS warning stream can evict the breadcrumbs that make real crash reports diagnosable — inverting the PR's goal.
Fix: Introduce a dedicated ReportCategory constant (e.g. ANALYTICS_TRANSPORT) for the Segment FFI reports and enable Warning only for that cell. This keeps the entire Segment downgrade benefit while leaving the 18 SentryTransactionManager sites muted. See inline comment for details.
Finding 2 — [P2] Callback warning is strictly redundant — Log is the honest level
Location: RustSegmentAnalyticsService.cs:278
The native report_error always pairs a failed operation with an ErrorCallback carrying the descriptive message. This Callback path adds no information the paired ErrorCallback doesn't already carry (just "Error" + operation ID). LogType.Log — already used three lines above for the success case — is the honest level. This also removes the only dependency on (ANALYTICS, Warning) in the DebugLog matrix, shrinking Finding 1's blast radius.
See the existing inline suggestion at line 278.
Finding 3 — [P2] Classifier string is owned by an external crate — name the source
Location: RustSegmentAnalyticsService.cs:249-258
The literal "Error executing send loop (will retry)" is emitted by AnalyticsEventSendDaemon in the segment crate pinned in .native/Cargo.toml. Nothing couples this C# string to that rev. It fails safe (back to error level), so not blocking — but the comment should name the source so whoever bumps the rev has a breadcrumb.
See the existing inline suggestion at line 258.
Finding 4 — [P2] Null-forgiving operators in test file lack justifying comment
Location: RustSegmentErrorReportingShould.cs (throughout)
The test file contains ~15 ! operators on reflection calls (e.g. SERVICE_TYPE.GetField("RETRY_REPORT", PRIVATE_STATIC)!). Per CLAUDE.md, null-forgiving operators require a justifying comment. A single top-of-class comment such as:
// Reflection lookups assert non-null — null means the production API changed and the test must be updated.
would satisfy the convention for all of them.
STEP 6 — Complexity assessment
SIMPLE. Touches 3 files with meaningful changes (1 service + 2 asset configs) plus a new test file and meta. Does not modify ECS systems, components, queries, async patterns, networking, plugin registration, or other complex subsystems.
STEP 7 — QA assessment
QA_REQUIRED: YES. Modifies runtime code (RustSegmentAnalyticsService.cs) that ships in the build and changes what severity reaches Sentry in production. Although no user-visible UI/rendering/gameplay change, the diagnostics routing change warrants validation that the correct errors still surface in Sentry.
STEP 8 — Non-blocking warnings
None. Main scene not modified.
Security review
No security issues found. No secrets exposed, no auth changes, no injection risks. The native IntPtr marshaling pattern is unchanged. The static data race on ONCE_PATTERN_ALREADY_CAUGHT (bare bool read/written from native callbacks without synchronization) is eliminated by the replacement RETRY_REPORT which is immutable after construction.
CI status
- ✅ EditMode tests: PASS (7/7)
- ✅ PlayMode tests: PASS
- ✅ Lint: PASS
- ⏳ Unity Cloud Build (windows64, macOS): In progress / queued
- ❌ Dependency Security Review: FAIL (encountered an error — unrelated to code; the
new-dependencylabel appears to be a mislabel)
Summary
| # | Severity | Finding | Status |
|---|---|---|---|
| 1 | P1 | Category-wide matrix flip un-mutes 18 unrelated, un-debounced SentryTransactionManager warning sites — can evict Sentry breadcrumbs |
❌ Blocking |
| 2 | P2 | Callback warning is strictly redundant with paired ErrorCallback — Log is the honest level |
Suggestion |
| 3 | P2 | Classifier string is owned by external segment crate — comment should name the source |
Suggestion |
| 4 | P2 | Test file ! operators lack justifying comment per CLAUDE.md |
Suggestion |
The analysis and error classification in this PR are excellent — the root-cause diagnosis is correct, the lossless/lossy distinction is well-reasoned, and the test coverage is thorough. The sole blocker is the unscoped matrix flip (Finding 1), which has an unintended blast radius on 18 unrelated warning sites.
REVIEW_RESULT: FAIL ❌
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Changes confined to Segment transport error classification and reporting-settings asset matrices
QA_REQUIRED: YES
Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub
| - Category: ANALYTICS | ||
| Severity: 2 |
There was a problem hiding this comment.
[P1] Category-wide matrix flip un-mutes 18 unrelated, un-debounced warning sites.
Enabling (ANALYTICS, Warning) in the production Sentry + DebugLog matrices also un-mutes 18 ReportHub.LogWarning(new ReportData(ReportCategory.ANALYTICS), …) calls in SentryTransactionManager.cs, none of which carry a debouncer.
Specifically, SentryWebRequestHandler.OnRequestStarted early-returns for file:// URLs without registering a transaction, but OnRequestFinished → Instance.StartSpan() and OnProcessDataFinished → Instance.EndCurrentSpan() + Instance.EndTransaction() still execute, producing 3 ANALYTICS warnings per such request that now reach both the player log and Sentry breadcrumbs.
Sentry's breadcrumb ring is bounded (default 100/scope), so an un-debounced ANALYTICS warning stream can evict the breadcrumbs that make real crash reports diagnosable — inverting the PR's goal.
Suggested fix: Introduce a dedicated ReportCategory constant (e.g. ANALYTICS_TRANSPORT) for the Segment FFI reports and enable Warning only for that cell in the matrices. This keeps the entire Segment downgrade benefit while leaving the 18 SentryTransactionManager sites muted.
| - Category: ANALYTICS | |
| Severity: 2 | |
| - Category: ANALYTICS | |
| Severity: 4 |
(Revert this entry to remove the category-wide Warning; re-add it under the new dedicated category instead.)
| // Native report_error always pairs a failed operation with an ErrorCallback delivery | ||
| // carrying the descriptive message — severity classification lives there. | ||
| if (response is not NativeMethods.Response.Success) | ||
| ReportHub.LogException(new Exception($"Segment operation {operationId} {type} failed with: {response}"), ReportCategory.ANALYTICS); | ||
| ReportHub.LogWarning(ReportCategory.ANALYTICS, $"Segment operation {operationId} {type} failed with: {response}"); |
There was a problem hiding this comment.
[P2] Redundant warning — Log is the honest severity for a strictly-duplicate report.
The native report_error always pairs a failed operation with an ErrorCallback delivery carrying the descriptive message. This Callback path adds no new information (just "Error" + operation ID). Since it's strictly redundant, LogType.Log — already used three lines above for the success case — is the honest level. This also removes the only dependency on (ANALYTICS, Warning) in the DebugLog matrix.
| // Native report_error always pairs a failed operation with an ErrorCallback delivery | |
| // carrying the descriptive message — severity classification lives there. | |
| if (response is not NativeMethods.Response.Success) | |
| ReportHub.LogException(new Exception($"Segment operation {operationId} {type} failed with: {response}"), ReportCategory.ANALYTICS); | |
| ReportHub.LogWarning(ReportCategory.ANALYTICS, $"Segment operation {operationId} {type} failed with: {response}"); | |
| if (response is not NativeMethods.Response.Success) | |
| ReportHub.Log(ReportCategory.ANALYTICS, $"Segment operation {operationId} {type} failed with: {response}"); |
| } | ||
|
|
||
| // The native send daemon retries a "(will retry)" send-loop failure without consuming the | ||
| // spooled item, so no events are lost and it must not bill the Sentry error budget. | ||
| // Every other native error is a potential loss signal and keeps exception-level reporting: | ||
| // instant_track_and_flush drops the event on send failure (bare "Network error", never | ||
| // spooled), a failed flush drops the already-extracted batch ("Cannot flush: ..."), and | ||
| // the "(will drop)" send-loop branch consumes the item. | ||
| private static bool IsRetriedSendLoopError(string message) => | ||
| message.Contains("Error executing send loop (will retry)"); |
There was a problem hiding this comment.
[P2] Classifier string is owned by an external crate — name the source for future maintainers.
The literal "Error executing send loop (will retry)" is emitted by AnalyticsEventSendDaemon in the segment crate (.native/Cargo.toml, pinned rev). A future crate bump can silently change the wording and the classifier stops matching. It fails safe (back to error-level), so not blocking — but the comment should name the source.
| } | |
| // The native send daemon retries a "(will retry)" send-loop failure without consuming the | |
| // spooled item, so no events are lost and it must not bill the Sentry error budget. | |
| // Every other native error is a potential loss signal and keeps exception-level reporting: | |
| // instant_track_and_flush drops the event on send failure (bare "Network error", never | |
| // spooled), a failed flush drops the already-extracted batch ("Cannot flush: ..."), and | |
| // the "(will drop)" send-loop branch consumes the item. | |
| private static bool IsRetriedSendLoopError(string message) => | |
| message.Contains("Error executing send loop (will retry)"); | |
| // The native send daemon retries a "(will retry)" send-loop failure without consuming the | |
| // spooled item, so no events are lost and it must not bill the Sentry error budget. | |
| // Every other native error is a potential loss signal and keeps exception-level reporting: | |
| // instant_track_and_flush drops the event on send failure (bare "Network error", never | |
| // spooled), a failed flush drops the already-extracted batch ("Cannot flush: ..."), and | |
| // the "(will drop)" send-loop branch consumes the item. | |
| // The matched text is emitted by AnalyticsEventSendDaemon in the `segment` crate pinned in | |
| // .native/Cargo.toml; re-verify this literal when that rev is bumped. | |
| private static bool IsRetriedSendLoopError(string message) => | |
| message.Contains("Error executing send loop (will retry)"); |
🚦 CI StatusWindows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below. Warnings not reduced: 12724 => 13123 — remove at least 400 warnings to merge. Warnings/errors in files changed by this PR (1)All Unity tests passed ✅
|
|
PR #9808, run #32260733528 Builds: Windows change, Windows baseline, macOS change, macOS baseline How to read this table
Intel Core i5
Exception breakdown
Apple M1
|
Problem
Segment analytics transport errors bill the Sentry error budget twice per failure and
report genuinely-lossless retries at error level - a ~88 events/week family across 7 Sentry
groups, with a Sentry alert that keeps auto-minting duplicate GitHub issues from it.
Root cause
Three reporting-path defects (scope corrected by adversarial review):
report_errorfans one failure into both C# callbacks -ErrorCallback(descriptive message) andCallback(bareErrorcode) - and bothcalled
LogException, so every failed operation was double-billed."Error executing send loop (will retry)") is emitted when thenative daemon retries WITHOUT consuming the spooled item - genuinely lossless, so
Warning is the honest severity. A prior once-per-session latch band-aid (its own TODO:
remove once the core issue is solved) is superseded.
ReportHub.LogWarning(ANALYTICS, ...)was matrix-dead: no shippedReportsHandlingSettingsasset enabled (ANALYTICS, Warning), so any downgrade wouldhave been total silence in real players.
Deliberately NOT reclassified (real data loss, kept at error level until the native fixes
land): instant-track
"Network error"(event dropped, never spooled - lost revenue-funnelevents),
"database is locked"flush failures (extracted batch dropped),"(will drop)",and
message too large.Fix (~35 production LOC + 6 asset lines)
RustSegmentAnalyticsService:IsRetriedSendLoopErrorclassifier matching only theprovably-lossless retry pattern →
LogWarning; everything else staysLogException; theonce-per-session latch is deleted.
Callback's not-Success report becomes a warning (thepaired
ErrorCallbackalways carries the descriptive error-level report for the sameoperation id).
ReportsHandlingSettings{Production,Development}.asset: enable (ANALYTICS, Warning) inthe production sentry + debugLog matrices and the development debugLog matrix, pinned by
an asset-loading test.
Net: ~66 of ~88 events/week reclassified with zero signal loss; the lossy patterns keep
billing the error budget by design. Follow-ups for the owning team (native): instant-track
enqueue fallback, flush ordering, sqlite busy_timeout.
Test
New EditMode
RustSegmentErrorReportingShould(7 tests): every send-loop retry reported asa warning (twice - proving the latch is gone), the duplicate operation-callback channel
downgraded, the shipped-matrix guard, and four
Keep...AsExceptionboundary tests pinningthe non-downgrade of the lossy/actionable patterns.
Validation
Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 3/7 as intended (exception-level
logs where warnings are expected; matrices don't enable ANALYTICS warnings at pin; the 4
preservation guards pass at pin) / GREEN PASS 7/7.
Related: #8574 (open - P13 send-loop retries; stops accruing), #7906 (stale-closed JVW
tracker - NOT silenced: instant-track loss keeps error-level signal; its duplicate-channel
echo is removed), #7867 (closed - the duplicate-channel form). Intentionally unaffected:
#8218 (locked-flush batch loss) and #7866 (message-too-large) keep accruing until the
native fixes land.
Includes inspection-warning cleanup in all touched files.
Fixes #7906