Skip to content

feat(processor): per-destination visibility at the destination-filter boundary - #7283

Open
mihir20 wants to merge 9 commits into
masterfrom
pipeline-inspector/02-per-destination-visibility
Open

feat(processor): per-destination visibility at the destination-filter boundary#7283
mihir20 wants to merge 9 commits into
masterfrom
pipeline-inspector/02-per-destination-visibility

Conversation

@mihir20

@mihir20 mihir20 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Part of Pipeline Inspector: gives per-destination visibility at the destination-filter boundary so fully- and partially-dropped events stop vanishing from the reporting funnel without a trace.

What changed

Single classification site. The per-destType destination discovery in the fan-out loop is replaced by one classifyDestinations call that splits a source's candidate destinations into available and excluded (with the exclusion reason and status code). A RETL-stamped destination_id narrows the candidate set before classification. isDestinationAvailable (the preprocess guard) and classifyDestinations agree by construction, which is pinned by tests.

Per-source destination snapshot. classifyDestinations runs per event, so it now consumes a sourceDestinations snapshot (destTypes DisplayName → definition for FilterClientIntegrations, byTypeName definition-name → enabled destinations in backend-config order) built once per source under a single config RLock, instead of taking two RLocks and re-scanning the source's destination list for every event. Only the event-dependent work (client-integration filtering, consent filtering) remains per event. Filtering semantics, ordering and RETL narrowing are unchanged.

One flag: Processor.earlyDestinationFilter (reloadable, default true).

  • true (default): behavior is byte-identical to today. Events with no surviving destination drop at the preprocess guard (pre tracking-plan) with the existing source-level destination_filter filtered/298 row. No new rows are emitted anywhere.
  • false: the preprocess guard is skipped, so every event reaches source hydration and tracking-plan validation, and the destination-filter decision moves to fan-out — which becomes the single drop point. Per-destination visibility comes with it unconditionally:
    • destination_enter succeeded/200 for every candidate destination (survivors and excluded alike), so destination_enter − filtered_* = user_transformer input holds per destination;
    • per-destination destination_filter rows for excluded candidates: filtered_integration/298 (integrations opt-out) and filtered_consent/297 (consent denied);
    • filtered_no_destination/298 (source-level, empty destination_id) for zero-candidate events — a source with no destinations, or a RETL event whose stamped destination is unavailable. Without this carve-out those events would disappear from the funnel entirely.

The flag is snapshotted once per batch in preprocessStage and threaded through the stage messages, so an in-flight batch never observes mixed values when the reloadable config flips.

Hard cutover, no dual-emit. With the flag false, the old source-level filtered row stops entirely — there is no intermediate state where the drop moves to fan-out but keeps the old row shape. Consumers of that row (reporting-service filteredEvents, dashboards) must be ready before the flip.

in_pu is left empty on all new rows. The field is slated for deprecation, so no chain value is computed for the rows introduced here, and the inPU values of pre-existing rows (gateway, tracking-plan, source-hydration, user-transformer) are untouched regardless of the flag.

Proc rebuild stage: disabled destinations keep flowing (review follow-up). DESTINATION_ENTER is recorded at fan-out, but a forked event whose destination got disabled before the proc rebuild stage consumed it used to be dropped there with no reporting row — leaving the reporting chain dangling. The rebuild stage now re-hydrates a disabled destination as-is and lets the event continue through the pipeline, so it reaches the router/batchrouter queue and is aborted there with the usual drain reporting ("destination is disabled"). A destination deleted from the config is still dropped at rebuild to a terminal Filtered status: without its config there is no destination type, hence no transformation to run and no router queue to store to.

New reloadable flag Processor.DestinationIsolation.dropEventsForDisabledDestAtProcRebuild (default false) opts back into the early drop for disabled destinations — useful for draining a proc-table backlog quickly after the user disables a misbehaving destination.

Behavior summary for a fully-dropped event

Processor.earlyDestinationFilter Drop point Rows emitted
true (default) preprocess guard, pre-TP old source-level filtered/298 only — identical to today
false fan-out, post-TP destination_enter ×N + filtered_* ×N; zero-candidate events emit filtered_no_destination/298

Side effects of flipping to false, for rollout notes: zero-destination events now incur source-hydration/TP transformer cost for sources that have those configured, and TP incoming/violation counts increase accordingly (arguably a fix — TP health no longer silently excludes events whose user opted out of every destination).

Tests

  • TestClassifyDestinations: unit coverage of the classifier — integrations/consent exclusion, RETL narrowing, ordering, disabled destinations excluded from the candidate set, and an invariant subtest that len(available) > 0 agrees with isDestinationAvailable across a case table.
  • TestDestinationVisibilityReporting: stage-level coverage of the fan-out reporting — enter/filter row shapes, the zero-candidate and RETL carve-outs, forked-destination enter rows, both flag states, and mid-flight flag flips on the same handle.
  • TestProcRebuildStage: config-drift coverage of the rebuild stage — a disabled destination flows through re-hydrated as-is by default, is dropped as Filtered when dropEventsForDisabledDestAtProcRebuild is set, and a deleted destination is always dropped.
  • integration_test/reporting_dropped_events: an end-to-end scenario driving one server through both flag states against a real Postgres reports table (preprocess row byte-compat, enter rows, partial exclusion, zero-candidate).

Linear Ticket

pipe-3286

Security

  • The code changed/added as part of this pull request won't create any security issues with how the software is being used.

@mihir20

mihir20 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Stack (merge bottom-up)

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.79%. Comparing base (f2655fb) to head (00ddf24).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7283      +/-   ##
==========================================
- Coverage   80.12%   79.79%   -0.34%     
==========================================
  Files         601      601              
  Lines       67217    67301      +84     
==========================================
- Hits        53857    53701     -156     
- Misses      10184    10434     +250     
+ Partials     3176     3166      -10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mihir20
mihir20 marked this pull request as draft August 18, 2026 07:03
@mihir20
mihir20 force-pushed the pipeline-inspector/01-dedup-metrics branch from 17bcee0 to 5445674 Compare August 19, 2026 10:57
@mihir20
mihir20 force-pushed the pipeline-inspector/02-per-destination-visibility branch 2 times, most recently from 2155a5b to 63dfc97 Compare August 19, 2026 11:29
Base automatically changed from pipeline-inspector/01-dedup-metrics to master August 19, 2026 12:14
@mihir20
mihir20 force-pushed the pipeline-inspector/02-per-destination-visibility branch from 63dfc97 to d4c03d8 Compare August 19, 2026 12:17
@mihir20
mihir20 marked this pull request as ready for review August 20, 2026 17:14
Comment thread utils/types/types.go
DrainEventCode = 410
SuccessEventCode = 200
DeliveredWithWarningCode = 296
ConsentDeniedEventCode = 297

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why does this require a new status code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's as per the original design proposal 😅. We are distinguishing filtering at a stage with different status codes for different statuses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am still trying to remember why we need this, I mean we are using different statuses already

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess I was influenced by the reportingv2 solution where counter labels are stable and we classify through special category codes. But not sure there is any benefit in the current design by inventing yet another status code.

Comment thread processor/processor.go Outdated
Comment thread processor/processor.go
@mihir20
mihir20 requested a review from atzoum August 25, 2026 15:10
… boundary

Add classifyDestinations as the single classification site at fan-out,
narrowing the candidate set to the RETL-stamped destination before
classification, and emit per-candidate destination_enter succeeded/200
rows (Reporting.destinationEnterMetrics.enabled) plus per-destination
filtered_integration/298 and filtered_consent/297 destination_filter
rows (Reporting.perDestinationFilterMetrics.enabled), with a
filtered_no_destination/298 carve-out for zero-candidate events.

Processor.earlyDestinationFilter (default true) gates the reorder: when
false the preprocess destination-filter guard is skipped so all events
reach source hydration and tracking-plan validation, fan-out becomes the
drop point, and the inPU chain labels are rewired to the new stage order
(source_hydration=gateway, tracking_plan_validator=srchyd|gateway,
user_transformer=destination_filter). The flag is snapshotted once per
batch and threaded through the stage messages so one in-flight batch
never observes mixed values. All flags are reloadable and default to
byte-identical-to-today behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

🔒 Scanned for secrets using gitleaks 8.28.0
The old two-level per-destType fan-out loop was collapsed into a single
loop over availableDestinations, but the inner block's indentation and
closing brace were left behind, breaking the build at processor.go:2577.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

🔒 Scanned for secrets using gitleaks 8.28.0
🔒 Scanned for secrets using gitleaks 8.28.0
🔒 Scanned for secrets using gitleaks 8.28.0
…ctions and update related logic

🔒 Scanned for secrets using gitleaks 8.28.0
…lter handling in reporting

🔒 Scanned for secrets using gitleaks 8.28.0
🔒 Scanned for secrets using gitleaks 8.28.0
… destination classification

🔒 Scanned for secrets using gitleaks 8.28.0
… rehydration and update related logic

🔒 Scanned for secrets using gitleaks 8.28.0
@mihir20
mihir20 force-pushed the pipeline-inspector/02-per-destination-visibility branch from cb7f42b to ef26068 Compare August 27, 2026 07:44
// so the reused transform stages operate on them unchanged.
// passed them.
//
// Config drift between fan-out and consume: a destination deleted from the config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In practice destinations are soft deleted, never hard deleted & soft deleted destinations arrive to rudder-server as disabled

Comment thread processor/processor.go
// survive filtering (available) and the ones excluded, with reason and status code, in the same
// order and applying the same filters the fan-out uses today: client-integration filtering first,
// then consent filtering. When specificDestID is set (RETL), the candidate set is narrowed to that
// single destination before classification — sibling destinations are never candidates and produce

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what are sibling destinations?

Comment thread utils/types/types.go
DrainEventCode = 410
SuccessEventCode = 200
DeliveredWithWarningCode = 296
ConsentDeniedEventCode = 297

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess I was influenced by the reportingv2 solution where counter labels are stable and we classify through special category codes. But not sure there is any benefit in the current design by inventing yet another status code.

@atzoum atzoum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@mihir20 I think that ideally, destination-specific reports should be persisted on the same tx that writes jobs to rt & batchrt jobsdb. Not a hard requirement though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants