Class RecordingCorrelationAnalyzer
- Namespace
- Kuestenlogik.Bowire.Recordings.Correlation
- Assembly
- Kuestenlogik.Bowire.Recordings.dll
Turns a recording into a correlated timeline (#539) — the single
canonical implementation behind both the workbench's Timeline tab
(via POST /api/recordings/correlate) and
bowire recording correlate.
A recording has no trace id: nothing in the .bwr format
carries one, and no capture path writes one. So the analyzer resolves
a signal in three tiers — a correlation header on
step.metadata, otherwise a shared id-shaped JSON leaf inferred
from the payloads, otherwise nothing, in which case the whole
recording is treated as one transaction and the result is a plain
protocol-lane time chart.
One key is not always enough (#545): a transaction that renames its identifier on the way across the landscape lights only the lanes that speak the chosen key. So a step the key missed gets a second and last edge — it joins the transaction when it shares a distinctive id-shaped value with a step the key did match, and the resulting RecordingCorrelationLink always names that value. See Kuestenlogik.Bowire.Recordings.Correlation.RecordingCorrelationAnalyzer.JoinThroughBridges(System.Collections.Generic.List{Kuestenlogik.Bowire.Mocking.BowireRecordingStep},System.Int64[],System.Collections.Generic.List{Kuestenlogik.Bowire.Recordings.Correlation.RecordingCorrelationEvent},Kuestenlogik.Bowire.Recordings.Correlation.RecordingCorrelationKey,System.Collections.Generic.List{System.String}) for the admissibility rule and what it turns down.
Pure and stateless by contract: no cache, no ring buffer, no injected dependency. Two calls with the same inputs produce the same output, which is what lets the CLI and the endpoint agree.
public static class RecordingCorrelationAnalyzer
- Inheritance
-
RecordingCorrelationAnalyzer
- Inherited Members
Fields
HeaderScoreBase
Score floor for a header candidate. Any correlation header beats
every inferred field candidate, because a header is an explicit
statement by the producer while a shared id is an inference.
Field scores are protocols * 1000 + steps, which cannot
realistically approach this, so the ordering is total without
clamping at int.MaxValue (which would make several header
candidates indistinguishable from each other).
public const int HeaderScoreBase = 1000000
Field Value
LaneFrameWarningThreshold
Frames per lane past which the timeline stops being readable and the renderer thins its ticks. Reported as a warning rather than silently truncating the model — the CLI still prints everything.
public const int LaneFrameWarningThreshold = 200
Field Value
MaxBridgeCarrierShare
The largest share of a recording's steps a value may appear on and still be treated as identifying ONE transaction.
public const double MaxBridgeCarrierShare = 0.6
Field Value
Remarks
Length alone is the wrong measure of "distinctive", and dangerously so: a session, tenant, customer or node id is long, high-entropy and sits on the same field name at both ends — the exact profile the strength score rewards most. A GUID session id would therefore be the single best bridge this analyzer can find, and it would fuse every request made in that session into one "transaction". A password change would become part of an order.
What separates a transaction key from a context key is not how it looks but how far it spreads: the transaction id appears on the steps belonging to that transaction, the session id appears on nearly everything. A value carried by most of the recording is describing the capture, not a transaction inside it.
Deliberately generous — this rejects the pathological case without second-guessing short recordings, where a legitimate id can easily touch half the steps.
MaxJoinDepth
Edges the join may walk, counting the seed match as the first. Fixed, not configurable: an unbounded walk over an id-rich recording relates everything to everything, which is worse than no join at all. Concretely — a step the key matched directly may bridge one hop further, and a step reached through a bridge never bridges onward.
public const int MaxJoinDepth = 2
Field Value
MinBridgeValueLength
How improbable a value has to be before it may bridge two steps
(#545). A bridge is an inference, not the operator's
choice, and unlike the seed key it gets no corroboration from a
matching field name — so the value has to carry its own weight.
Anything shorter than this has too few possible values for a
collision to be surprising: 1, 42, true and
OK are not evidence, and a harbour capture where
portCallId, craneId and a dock number are all
1 is precisely the case that must not join.
public const int MinBridgeValueLength = 6
Field Value
Methods
Analyze(BowireRecording, RecordingCorrelationKey?)
Place every step (and every streamed frame) of the recording on a shared time axis and verdict each one against the resolved key.
public static RecordingCorrelationTimeline Analyze(BowireRecording recording, RecordingCorrelationKey? key = null)
Parameters
recordingBowireRecordingThe recording to analyse.
keyRecordingCorrelationKeyAn explicit key from the operator or from CI. When null, the recording's persisted Correlation wins, then the top-scoring suggestion, then nothing.
Returns
ResolveSource(string?)
Decide where a key came from when the caller did not say. A name that reads as one of the recognised correlation headers is a header key; anything else is a payload leaf.
public static string ResolveSource(string? name)
Parameters
namestring
Returns
Suggest(BowireRecording)
Every key this recording would accept, best first. Empty when no value is shared by at least two steps.
public static IReadOnlyList<RecordingCorrelationCandidate> Suggest(BowireRecording recording)
Parameters
recordingBowireRecording