This document provides comprehensive documentation for the B2C Policy Log Analyzer feature, which allows developers to query, visualize, and debug Azure AD B2C Journey Recorder logs from Application Insights.
Note: This is part of the TraceFlow-IEF application. For installation and getting started, see QUICK_START.md. For overall architecture, see ARCHITECTURE.md.
- Overview
- Architecture
- Data Flow
- Key Components
- Log Processing Pipeline
- Trace Parsing System
- UI Components
- Type System
- Troubleshooting
The Log Analyzer feature enables debugging of Azure AD B2C Custom Policies by:
- Querying Journey Recorder logs from Application Insights
- Merging truncated log messages that were split across multiple rows
- Parsing B2C-specific "clips" (structured event data)
- Grouping logs into distinct user flows
- Generating step-by-step trace visualizations
- Displaying statebag and claims data at each step
| Term | Description |
|---|---|
| Journey Recorder | B2C's built-in diagnostic logging system that emits structured events |
| Clips | JSON-structured events within a log message (Headers, Transition, HandlerResult, etc.) |
| Statebag | B2C's internal state dictionary tracking policy execution context |
| Correlation ID | Unique identifier linking all logs from a single policy execution |
| User Flow | A complete policy execution from start to finish |
| Orchestration Step | A numbered step in the policy's user journey |
flowchart TB
subgraph UI["UI LAYER"]
direction LR
CF[credentials-form<br/>App ID, API Key<br/>Timespan, Search]
RT[results-table<br/>Log listing with<br/>flow grouping]
TT[trace-timeline<br/>Step-by-step visualization<br/>with statebag inspection]
DV[detail-viewer<br/>Raw JSON & structured clip view]
end
subgraph STATE["STATE LAYER (Zustand)"]
LS[log-store.ts<br/>• credentials<br/>• preferences<br/>• logs: LogRecord[]<br/>• userFlows: UserFlow[]<br/>• traceSteps: TraceStep[]<br/>• executionMap<br/>• selectedLog, selectedFlow]
end
subgraph PROCESSING["PROCESSING LAYER"]
AIP[app-insights-processor.ts<br/>1. mapRows<br/>2. aggregate<br/>3. toLogRecord]
subgraph TRACE["trace/ module"]
TP[TraceParser]
INT[Interpreters]
FA[FlowAnalyzer]
SA[StatebagAccumulator]
end
end
subgraph API["API LAYER"]
AIC[application-insights-client.ts<br/>• query<br/>• fetchCompleteFlows<br/>• KQL query generation]
end
AZURE[(Azure Application Insights<br/>REST API)]
UI --> STATE
STATE --> PROCESSING
AIP --> TRACE
PROCESSING --> API
API --> AZURE
flowchart TD
UI[User Input] --> FL[fetchLogs]
FL --> Q1[ApplicationInsightsClient.query]
subgraph Phase1["Phase 1: Initial Query"]
Q1 --> KQL1["KQL Query:<br/>traces<br/>| where EventName startswith 'JourneyRecorder'<br/>| where isnotempty UserJourney<br/>| order by timestamp desc<br/>| take maxRows"]
end
KQL1 --> EXT[Extract unique correlationIds]
subgraph Phase2["Phase 2: Complete Flow Fetch"]
EXT --> KQL2["KQL Query:<br/>traces<br/>| where CorrelationId in ids<br/>| order by timestamp asc"]
end
KQL2 --> RESULT[Complete LogRecords]
Why Two Phases?
- Initial query may return partial flows (e.g., only the last 50 logs of a 100-log flow)
- Phase 2 ensures we have ALL logs for each flow, enabling complete trace reconstruction
flowchart TD
RAW[Raw API Response<br/>AppInsightsTable] --> MAP
subgraph MAP[mapRows]
M1[Extract columns by name]
M2[Parse customDimensions JSON]
M3[Create RawLogRow objects]
end
MAP --> AGG
subgraph AGG[aggregate]
A1[Sort by timestamp ascending]
A2["Detect truncated messages:<br/>- Starts with '[' = new entry<br/>- Ends with ']' = complete entry"]
A3[Concatenate fragments]
A4[Track composite IDs]
end
AGG --> REC
subgraph REC[toLogRecord]
R1[Parse JSON payload]
R2[Extract clips array]
R3[Type clips: Headers, HandlerResult]
R4[Build LogRecord with metadata]
end
REC --> OUT[LogRecord array]
Application Insights truncates messages at ~32KB. B2C Journey Recorder logs often exceed this, resulting in split messages:
Log Entry 1 (timestamp: T1):
message: '[{"Kind":"Headers","Content":{"PolicyId":"B2C_1A_..."...<truncated>
Log Entry 2 (timestamp: T2, microseconds after T1):
message: ...}},{"Kind":"HandlerResult","Content":{...}}]'
Aggregation Algorithm:
flowchart TD
START[For each row] --> CHECK1{Starts with '[' ?}
CHECK1 -->|Yes| NEW[Start new accumulator<br/>Save row as entryStartRow]
CHECK1 -->|No| CHECK2{entryStartRow exists?}
CHECK2 -->|Yes| APPEND[Append to accumulator]
CHECK2 -->|No| PLAIN[Plain text message<br/>Create entry immediately]
NEW --> CHECK3{Ends with ']' ?}
APPEND --> CHECK3
CHECK3 -->|Yes| CREATE[Create AggregatedLogRow<br/>with merged message<br/>Reset accumulator]
CHECK3 -->|No| NEXT[Continue to next row]
CREATE --> NEXT
PLAIN --> NEXT
NEXT --> START
Location: src/lib/api/application-insights-client.ts
Responsibilities:
- Build KQL queries for B2C Journey Recorder events
- Execute queries against Application Insights REST API
- Handle two-phase query for complete flows
Key Methods:
| Method | Purpose |
|---|---|
query() |
Initial search query with filters |
fetchCompleteFlows() |
Fetch all logs for given correlation IDs |
extractPrimaryTable() |
Extract result table from API response |
Location: src/lib/app-insights-processor.ts
Responsibilities:
- Convert raw API response to typed LogRecord objects
- Merge truncated log messages
- Parse clip arrays with proper typing
Processing Steps:
- mapRows() - Map raw row arrays to structured objects
- aggregate() - Merge truncated messages by detecting
[and]boundaries - toLogRecord() - Parse JSON, extract and type clips
Location: src/stores/log-store.ts
State Structure:
interface LogStore {
// Credentials & Preferences
credentials: { applicationId: string; apiKey: string };
preferences: { maxRows: number; timespan: string };
// Log Data
logs: LogRecord[];
selectedLog: LogRecord | null;
userFlows: UserFlow[];
selectedFlow: UserFlow | null;
// Trace Data
traceSteps: TraceStep[];
executionMap: TraceExecutionMap;
activeStepIndex: number | null;
// UI State
isLoading: boolean;
error: string | null;
searchText: string;
}Key Actions:
| Action | Purpose |
|---|---|
fetchLogs() |
Execute query and process results |
setSelectedLog() |
Select log and auto-generate trace |
selectFlow() |
Switch to different user flow |
generateTrace() |
Manually regenerate trace from logs |
setActiveStep() |
Navigate trace timeline |
The trace module (src/lib/trace/) parses clips into a step-by-step execution trace.
flowchart TD
subgraph TP["TraceParser"]
CA[ClipAggregator<br/>Groups clips by handler]
subgraph IR["InterpreterRegistry"]
I1[ProcessOrch<br/>Interpreter]
I2[ValidateTp<br/>Interpreter]
I3[LoadClaims<br/>Interpreter]
I4[ClaimsTrans<br/>Interpreter]
I5[SelfAsserted<br/>Interpreter]
I6[REST API<br/>Interpreter]
end
SA[StatebagAccumulator<br/>Tracks state changes across steps]
EMB[ExecutionMapBuilder<br/>Maps graph node IDs to execution status]
end
CA --> IR
IR --> SA
SA --> EMB
EMB --> RESULT[TraceParseResult]
Each B2C handler type has a specialized interpreter:
| Handler | Interpreter | Purpose |
|---|---|---|
ProcessOrchestrationStep |
ProcessOrchStepInterpreter |
New orchestration step |
ValidateTechnicalProfile |
ValidateTechnicalProfileInterpreter |
TP execution |
LoadClaimsViaTp |
LoadClaimsInterpreter |
Claims provider calls |
ClaimsTransformation |
ClaimsTransformationInterpreter |
CT execution |
SelfAssertedPage |
SelfAssertedInterpreter |
User input pages |
RestAPIProvider |
RestApiInterpreter |
External API calls |
Interpreter Interface:
interface Interpreter {
readonly handlerName: string;
interpret(context: InterpretContext): InterpretResult;
}
interface InterpretResult {
success: boolean;
createStep?: boolean; // Should create new trace step
actionHandler?: string; // Handler name for display
technicalProfiles?: string[]; // TPs executed
statebagUpdates?: Record<string, string>;
claimsUpdates?: Record<string, string>;
stepResult?: "Success" | "Error" | "Skipped";
// ... many more fields
}Location: src/lib/trace/services/flow-analyzer.ts
Groups logs into distinct user flows. A new flow starts when:
- A new
correlationIdis encountered ORCH_CS = 0is detected (flow initialization)- Step number goes backwards (user navigated back)
Exception: SubJourney invocation can reset ORCH_CS to 0 without starting a new flow.
Form for entering Application Insights credentials:
- Application ID (GUID)
- API Key
- Timespan (ISO 8601 duration)
- Max rows
- Search text (optional)
Displays query results:
- Flow selector (grouped by correlationId)
- Log list with timestamps and policy IDs
- Status indicators (completed, errors, cancelled)
Step-by-step trace visualization:
- Sequential step cards
- Active step highlighting
- Navigation controls (prev/next)
- Statebag inspector integration
Displays statebag and claims at selected step:
- Key-value pairs
- Diff highlighting (what changed)
- Search/filter capability
Raw log inspection:
- Formatted JSON view
- Clip breakdown
- Copy functionality
Location: src/types/logs.ts
interface LogRecord {
id: string;
timestamp: Date;
policyId: string;
correlationId: string;
cloudRoleInstance?: string;
rawIds: string[]; // IDs of merged log entries
payloadText: string; // Raw message text
parsedPayload: unknown; // Parsed JSON
clips: ClipsArray; // Typed clip array
customDimensions: CustomDimensions;
}Location: src/types/journey-recorder.ts
type Clip =
| HeadersClip
| TransitionClip
| PredicateClip
| ActionClip
| HandlerResultClip
| FatalExceptionClip
| GenericClip;
interface HeadersClip {
Kind: "Headers";
Content: {
UserJourneyRecorderEndpoint: string;
CorrelationId: string;
EventInstance: string; // "Event:AUTH", "Event:SELFASSERTED", etc.
TenantId: string;
PolicyId: string;
};
}
interface HandlerResultClip {
Kind: "HandlerResult";
Content: {
Result: boolean;
PredicateResult?: string;
RecorderRecord?: RecorderRecord;
Statebag?: Statebag;
Exception?: ExceptionContent;
};
}Location: src/types/trace.ts
interface TraceStep {
sequenceNumber: number;
timestamp: Date;
stepOrder: number; // Orchestration step number
journeyContextId: string; // Policy ID
actionHandler: string; // Handler that executed
technicalProfiles: string[]; // TPs involved
result: "Success" | "Error" | "Skipped" | "Pending";
errorMessage?: string;
statebagSnapshot: Record<string, string>;
claimsSnapshot: Record<string, string>;
duration?: number; // ms to next step
// ... many more fields
}
interface UserFlow {
id: string;
correlationId: string;
policyId: string;
startTime: Date;
endTime: Date;
stepCount: number;
completed: boolean;
hasErrors: boolean;
cancelled: boolean;
subJourneys: string[];
logIds: string[];
}- Check credentials: Ensure Application ID and API Key are correct
- Check timespan: Logs may be outside the query window
- Check filters: Search text or policy filters may be too restrictive
The aggregate() function merges logs by detecting [ (start) and ] (end) boundaries. If logs aren't merging:
- Check timestamp sorting: Logs must be sorted chronologically
- Check message format: Messages must be valid JSON arrays starting with
[ - Enable debug logging: Temporarily add console.logs in
aggregate()
- Check event types: Only
Event:AUTH,Event:API,Event:SELFASSERTED, andEvent:ClaimsExchangeare processed - Check clip structure: HandlerResult clips must have proper Statebag entries
- Check interpreter: The handler may not have a registered interpreter
- Reduce maxRows: Large result sets slow down processing
- Use search filters: Narrow down to specific policies or users
- Check browser memory: Very large traces may exhaust memory
Browser DevTools:
- Network tab: Verify API requests/responses
- Console: Check for processing errors
- React DevTools: Inspect store state
Log Store Debugging:
// In browser console:
const store = window.__ZUSTAND_LOG_STORE__;
console.log(store.getState());