Skip to content

Add session ID crypto, refactor chat memory, deduplication, simple analytics - #21

Merged
WilliamAGH merged 19 commits into
mainfrom
dev
Feb 11, 2026
Merged

Add session ID crypto, refactor chat memory, deduplication, simple analytics#21
WilliamAGH merged 19 commits into
mainfrom
dev

Conversation

@WilliamAGH

Copy link
Copy Markdown
Owner

This pull request introduces several improvements and refactorings across both the frontend and backend, focusing on session management, document ingestion deduplication, chat memory handling, and health check reliability. The changes enhance robustness, thread safety, and maintainability, while also improving test coverage and analytics integration.

Backend Improvements

Chat Memory Service Refactor:

  • Refactored ChatMemoryService to use a new SessionConversation class, ensuring thread-safe and consistent management of per-session chat history and turns. This replaces multiple concurrent maps with a single map, and synchronizes updates for each session. Also, added a hasSession method for session existence checks. [1][2][3][4][5]

Document Ingestion Deduplication:

  • Enhanced chunk ingestion logic in ChunkProcessingService to not only skip already-ingested chunks, but also re-ingest if associated metadata (title or package) has changed. This is achieved by expanding the HashIngestionLookup interface and updating its usage. [1][2][3][4]
  • Updated DocsIngestionService to mark hashes as ingested with metadata, and added a helper to extract metadata for this purpose.

Health Check Robustness:

  • Improved ExternalServiceHealth to clarify unhealthy/retry-due states, prevent duplicate health checks, and ensure scheduled checks work correctly even if the service is unhealthy. [1][2][3][4][5]
  • Added a call to trigger retry checks in QdrantHealthIndicator when the service is unhealthy and backoff has elapsed.

Configuration:

  • Increased default embedding vector dimensions from 1536 to 4096 in AppProperties.Embeddings.

Frontend Improvements

Session ID Generation and Testing:

  • Refactored session ID generation to use crypto.randomUUID if available, falling back to crypto.getRandomValues or a padded Math.random output. Added comprehensive tests for all fallback paths. [1][2][3]

Analytics Integration:

  • Added a Vite plugin to inject Simple Analytics scripts dynamically based on environment mode, improving analytics integration without manual HTML edits. [1][2]

These changes collectively improve reliability, maintainability, and observability across the application.

Chunk deduplication only checked hash markers derived from URL, chunk index, and
chunk text. When title or package metadata changed without content changes,
ingestion skipped those chunks and left stale metadata in the vector layer.
This change persists title/package metadata in hash marker payloads and compares
stored marker metadata during dedup checks. Existing hashes are now reprocessed
when metadata changes, while unchanged hashes still skip as before.
- Add metadata-aware hash marker read/write logic in LocalStoreService
- Add hasHashMetadataChanged(...) and metadata parsing for existing markers
- Update ChunkProcessingService to reprocess hash hits on metadata drift
- Pass title/package metadata when marking ingested hashes in ingestion flows
- Add regression tests for both metadata-drift reingest and unchanged skip
Health recovery logic could stall after transient outages: health snapshots did
not trigger retries, unhealthy scheduled checks were effectively bypassed, and
parallel retry paths could inflate failure state while risking backoff overflow.
This change ensures retry evaluation happens when health is polled, gates health
checks to one in-flight check per service status, and uses overflow-safe capped
backoff progression.
- Trigger retry evaluation from QdrantHealthIndicator.health()
- Run unhealthy retry evaluation in scheduled health checks
- Add in-progress check gating in ExternalServiceHealth.ServiceStatus
- Replace Math.pow duration growth with overflow-safe capped doubling
- Add tests for retry trigger behavior and concurrency/overflow handling
Chat memory maintained two separate per-session structures (messages and turns)
with non-atomic updates. Under concurrency, ordering could diverge, and session
validation semantics relied on side-effect-prone access patterns.
This refactor introduces a single per-session conversation object with
synchronized atomic updates, plus explicit session recognition checks for
validation endpoints.
- Replace dual-map storage with SessionConversation aggregate per session
- Update user/assistant writes to atomically update history and turns together
- Add hasSession(String) for side-effect-free recognition checks
- Update /api/chat/session/validate to avoid unknown-session creation side effects
- Clarify session response docs and add concurrency/session validation tests
Some HTTP and provider exception types can expose null status text,
headers, or response payload values. The previous formatter called
methods on those values without guarding for null, which could throw
while we were already handling an upstream failure.
This change hardens exception detail assembly so error reporting stays
reliable under malformed or partial exception metadata.
- Guard RestClient status text and response body before blank checks
- Guard WebClient status text, response body, and headers before use
- Guard OpenAI exception headers/body values before appending
- Add regression test covering null status text handling
Rate-limit window expiry was clearing consecutive failure counters during
availability checks, and rate-limit events were not always incrementing
total failure metrics. That made resilience telemetry and backoff
behavior less trustworthy after throttling events.
This change keeps failure history intact across expiry boundaries and
records every rate-limit event consistently.
- Increment total failure count when recording rate-limit events
- Stop resetting consecutive failures when availability window expires
- Add focused tests for expiry behavior and total failure increments
Embedding configuration default dimensions and request hint behavior were
out of sync for mixed provider/model usage. The client also sent
dimensions indiscriminately, which can break providers that do not
accept that field for their embedding models.
This change aligns defaults with configured expectations and applies
dimension hints only for models that support override semantics.
- Set embedding default dimensions to 4096 in AppProperties
- Add model-aware dimension override gating for text-embedding-3 models
- Keep request construction typed while conditionally applying dimensions
- Add regression tests for include/omit dimension request behavior
Session identifiers were built from timestamp plus Math.random-only
suffixes, which are weaker and less stable across runtimes with varying
crypto support. This could increase collision risk and reduce entropy in
high-throughput client sessions.
This change introduces a deterministic fallback chain that uses the
strongest available browser/runtime primitive first, then degrades
explicitly.
- Add random segment generator with crypto.randomUUID primary path
- Fallback to crypto.getRandomValues hex encoding when UUID is unavailable
- Keep explicit padded Math.random fallback for legacy runtimes
- Add unit tests covering all three entropy-source branches
Pre-commit formatting (spotlessApply) reformatted two files during earlier
commits, leaving non-functional line-wrap and trailing-line diffs unstaged.
These diffs are noise-only and should be isolated so functional commits remain
clean and fully focused on behavior changes.
This commit captures only formatter output and does not alter runtime behavior
or test intent.
- Wrap long method-call lines to formatter-compliant style
- Normalize chained append formatting in marker payload builder
- Remove trailing blank line in test file
- Keep logic and assertions unchanged
The session utility tests repeated identical fake-timer initialization in
each case. This was introduced by formatter/hook interactions and left as
an unstaged follow-up change.
This commit centralizes shared timer setup in beforeEach to keep tests
clear and reduce repetition without changing assertions or coverage.
- Add beforeEach with fixed test clock initialization
- Remove duplicated timer setup lines from individual test cases
- Keep existing test scenarios and expectations unchanged
The site needs privacy-friendly analytics tracking. Rather than hardcoding
a script tag in index.html, this uses Vite's built-in transformIndexHtml
hook to inject the correct script at build time — latest.dev.js in
development (no-op) and latest.js in production (real tracking).
- Switch defineConfig from object to function form to access ConfigEnv.mode
- Add simple-analytics plugin using transformIndexHtml to return HtmlTagDescriptor
- Extract CDN base URL to SIMPLE_ANALYTICS_CDN named constant
- Remove stale "Serve from root" comment on base property
@WilliamAGHWilliamAGH self-assigned this Feb 10, 2026
@coderabbitai

coderabbitaiBot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Integrated analytics tracking for improved insights into application usage.
    • Enhanced document ingestion with metadata preservation and change detection.
  • Bug Fixes

    • Improved null-safety in exception handling to prevent runtime errors.
    • Refined session validation logic for better accuracy.
  • Tests

    • Added comprehensive test coverage for session handling, health checks, and document processing.

Walkthrough

Consolidates session-random logic and tests, adds mode-aware Vite analytics injection, centralizes per-session chat state, implements metadata-aware ingestion deduplication, adds robust external-service retry/backoff and health checks, hardens exception null-safety, and expands tests across these areas.

Changes

Cohort / File(s)Summary
Frontend Session Generation
frontend/src/lib/utils/session.ts, frontend/src/lib/utils/session.test.ts
Extracted random-part generation into createSessionRandomPart() with crypto.randomUUID, getRandomValues, and Math.random fallback. Added Vitest-based tests covering all crypto availability scenarios and fixed-time assertions.
Frontend Build & Analytics
frontend/vite.config.ts
Switched to function-form Vite config accepting mode. Added simple-analytics inline plugin to inject a mode-dependent script tag from SIMPLE_ANALYTICS_CDN.
Chat Memory & Session Validation
src/main/java/.../service/ChatMemoryService.java, src/main/java/.../web/ChatController.java, src/main/java/.../web/SessionValidationResponse.java, src/test/java/.../service/ChatMemoryServiceTest.java, src/test/java/.../web/ChatControllerSessionValidationTest.java
Replaced per-type maps with a synchronized SessionConversation per session, added hasSession(String). Updated validateSession() to short-circuit for unknown sessions and provide distinct messages for found vs. empty sessions. Added concurrency tests for memory consistency and hasSession behavior.
Ingestion Deduplication & Metadata
src/main/java/.../service/LocalStoreService.java, src/main/java/.../service/ChunkProcessingService.java, src/main/java/.../service/DocsIngestionService.java, src/main/java/.../service/ingestion/LocalDocsFileIngestionProcessor.java, src/test/java/.../service/ChunkProcessingServiceTest.java, src/test/java/.../service/LocalStoreServiceTest.java
Introduced Base64-encoded title/package metadata in hash markers. Added hasHashMetadataChanged(hash,title,packageName) and markHashIngested(hash,title,packageName). Adjusted chunk/document processing to reingest when metadata changed and updated tests to assert reingest vs skip behavior.
External Service Health & Backoff
src/main/java/.../service/ExternalServiceHealth.java, src/main/java/.../config/QdrantHealthIndicator.java, src/test/java/.../service/ExternalServiceHealthTest.java, src/test/java/.../config/QdrantHealthIndicatorTest.java
Reworked health-check logic to centralize retry/backoff with in-flight check tracking, safe exponential backoff cap, guarded check-start/shouldRetry flows, and clearer health snapshot messaging. Qdrant health indicator now triggers a pre-check call. Added unit tests covering backoff, concurrency, and retry gating.
Embedding Client & Config
src/main/java/.../service/OpenAiCompatibleEmbeddingClient.java, src/main/java/.../config/AppProperties.java, src/test/java/.../service/OpenAiCompatibleEmbeddingClientTest.java
Conditionally include dimensions in embedding request for "text-embedding-3" models via supportsDimensionOverride(). Updated default embedding dimensions in config from 1536 to 4096. Added tests verifying dimension inclusion/omission per model.
Local Store Integration (ingestion callers)
src/main/java/.../service/ingestion/LocalDocsFileIngestionProcessor.java
Updated ingestion pipeline to fetch document metadata (title, package) via DocumentFactory.metadataText(...) and pass them to LocalStoreService.markHashIngested(...).
Exception Safety
src/main/java/.../web/ExceptionResponseBuilder.java, src/test/java/.../web/ExceptionResponseBuilderTest.java
Added null-safety guards for status text, response body, and headers when building exception descriptions; tests ensure null statusText no longer throws.
Rate Limiting
src/main/java/.../service/RateLimitState.java, src/test/java/.../service/RateLimitStateTest.java
Increment totalFailures on rate-limit events and stop resetting consecutiveFailures on expiry in availability check. Added tests to assert counter behavior and non-reset semantics.
Misc / Utilities
src/main/java/.../service/DocumentFactory.java, config/spotbugs/spotbugs-exclude.xml
Added DocumentFactory.metadataText(document, key) helper to safely read metadata as a string. Added SpotBugs exclusion for ExternalServiceHealth to suppress CRLF_INJECTION_LOGS warning.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"A tiny crypto seed, a careful backoff dance,
Analytics sneaks in, giving pages a glance.
Sessions gather safely in a single place,
Metadata keeps duplicates from taking up space.
Tests hum a tune—robust, precise, and fair."

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 28.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately summarizes the main changes: session ID improvements, chat memory refactoring, deduplication enhancements, and analytics integration are all present in the changeset.
Description check✅ PassedThe description provides clear context for backend and frontend improvements, connecting changes to their intended benefits and including helpful internal references to relevant sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/test/java/com/williamcallahan/javachat/web/ChatControllerSessionValidationTest.java (1)

28-30: Consider extracting common setup (optional, nice-to-have).

Both tests instantiate ChatMemoryService and ChatController identically. If you add more tests later, a @BeforeEach setup method would reduce duplication:

privateChatMemoryServicechatMemoryService;
privateChatControllerchatController;
`@BeforeEach`
voidsetUp() {
chatMemoryService = newChatMemoryService();
chatController = newChatController(
null, chatMemoryService, null, null, null, newExceptionResponseBuilder(), newAppProperties());
}

That said, with only two tests, the current approach keeps each test self-contained and easy to understand at a glance. Either way works! 🙂

Also applies to: 54-56


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot changed the title mergeAdd session ID crypto, refactor chat memory, deduplicationFeb 10, 2026
@coderabbitaicoderabbitaiBot added enhancement New feature or request java Pull requests that update java code refactor Code refactoring labels Feb 10, 2026
@WilliamAGHWilliamAGH changed the title Add session ID crypto, refactor chat memory, deduplicationAdd session ID crypto, refactor chat memory, deduplication, simple analyticsFeb 10, 2026

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/com/williamcallahan/javachat/service/ingestion/LocalDocsFileIngestionProcessor.java (1)

369-389: 🛠️ Refactor suggestion | 🟠 Major

Use constants for metadata keys.
Avoid new magic strings for "title" and "package".

♻️ Suggested refactor
 public class LocalDocsFileIngestionProcessor {
private static final Logger log = LoggerFactory.getLogger(LocalDocsFileIngestionProcessor.class);
private static final Logger INDEXING_LOG = LoggerFactory.getLogger("INDEXING");
private static final String FILE_URL_PREFIX = "file://";
private static final String API_PATH_SEGMENT = "${app.ingestion.api-path-segment:/api/}";
+ private static final String METADATA_KEY_TITLE = "title";+ private static final String METADATA_KEY_PACKAGE = "package";
@@
- String title = metadataText(doc, "title");- String packageName = metadataText(doc, "package");+ String title = metadataText(doc, METADATA_KEY_TITLE);+ String packageName = metadataText(doc, METADATA_KEY_PACKAGE);

As per coding guidelines, “No inline numbers (except 0, 1, -1) or strings; define named constants (no magic literals).”

src/main/java/com/williamcallahan/javachat/service/DocsIngestionService.java (1)

154-176: ⚠️ Potential issue | 🟡 Minor

Align metadata handling with naming/constant rules.

doc is a banned abbreviation, and the new metadata keys/empty string should be named constants. A small refactor keeps this compliant and clearer.

♻️ Suggested fix
@@
public class DocsIngestionService {
private static final Logger INDEXING_LOG = LoggerFactory.getLogger("INDEXING");
private static final String API_PATH_SEGMENT = "/api/";
private static final Duration HTTP_CONNECT_TIMEOUT = Duration.ofSeconds(30);
+ private static final String METADATA_HASH_KEY = "hash";+ private static final String METADATA_TITLE_KEY = "title";+ private static final String METADATA_PACKAGE_KEY = "package";+ private static final String METADATA_EMPTY_TEXT = "";
@@
- for (org.springframework.ai.document.Document doc : documents) {- Object hashMetadata = doc.getMetadata().get("hash");+ for (org.springframework.ai.document.Document document : documents) {+ Object hashMetadata = document.getMetadata().get(METADATA_HASH_KEY);
if (hashMetadata == null) {
continue;
}
- String title = metadataText(doc, "title");- String packageName = metadataText(doc, "package");+ String title = metadataText(document, METADATA_TITLE_KEY);+ String packageName = metadataText(document, METADATA_PACKAGE_KEY);
try {
localStore.markHashIngested(hashMetadata.toString(), title, packageName);
@@
private String metadataText(org.springframework.ai.document.Document document, String metadataKey) {
Object metadataRaw = document.getMetadata().get(metadataKey);
if (metadataRaw == null) {
- return "";+ return METADATA_EMPTY_TEXT;
}
return metadataRaw.toString();
}

As per coding guidelines: “Banned abbreviations: … doc …” and “No inline numbers (except 0, 1, -1) or strings; define named constants (no magic literals).”

🤖 Fix all issues with AI agents
In `@frontend/vite.config.ts`:
- Around line 4-24: Extract the inline Simple Analytics plugin from
vite.config.ts into a new module by moving SIMPLE_ANALYTICS_CDN and the plugin
object into a dedicated exported function simpleAnalyticsPlugin(mode: string):
Plugin; implement the same transformIndexHtml logic there (including selecting
latest.dev.js vs latest.js and returning the script tag with async and src),
export the function, then import and call simpleAnalyticsPlugin(mode) in the
plugins array inside defineConfig to replace the current inline plugin.
In
`@src/main/java/com/williamcallahan/javachat/service/ChunkProcessingService.java`:
- Around line 186-190: The local variable named doc in the block using
hashAlreadyIngested/hashIngestionLookup and
documentFactory.createDocumentWithPages should be renamed to a domain-specific
identifier (e.g., document) to follow the banned-abbreviations rule; update the
declaration and every usage (the variable passed to pageDocuments.add and any
later references) so that document replaces doc consistently in the method
containing hashAlreadyIngested, hashIngestionLookup.hasMetadataChanged,
documentFactory.createDocumentWithPages, and pageDocuments.add.
In `@src/main/java/com/williamcallahan/javachat/web/ChatController.java`:
- Around line 313-321: The inline user-facing strings in ChatController (e.g.,
"Session not found on server", "Session found", "Session found but empty")
violate the no-inline-strings rule; replace them with named constants (e.g.,
SESSION_NOT_FOUND_MSG, SESSION_FOUND_MSG, SESSION_FOUND_EMPTY_MSG) defined
either as private static final String fields inside ChatController or in a
centralized Messages/constants class, then use those constants in the
SessionValidationResponse constructors (keep existing variables sessionId,
turnCount, exists and the ResponseEntity return logic unchanged).
In
`@src/test/java/com/williamcallahan/javachat/web/ChatControllerSessionValidationTest.java`:
- Around line 19-57: Rename the generic local variable names (e.g., change
"response" to a specific name like "badRequestResponse" or
"unknownSessionResponse") in the tests
validateSession_returnsBadRequestWhenSessionIdIsBlank and
validateSession_doesNotCreateUnknownSessionAndReportsRecognizedSessionHistory,
and move repeated string literals and session id values (e.g., "Session ID is
required", "Session not found on server", "Session found", "unknown-session-id",
"recognized-session-id", and the stored message) into private static final
constants at the top of ChatControllerSessionValidationTest; update all usages
(assertEquals, validateSession calls, addUser call, etc.) to reference those
constants and adjust variable names to be descriptive rather than the banned
generic "response".
🧹 Nitpick comments (2)
src/test/java/com/williamcallahan/javachat/service/RateLimitStateTest.java (1)

24-29: Solid test setup! 🧪

Registering JavaTimeModule is essential for Jackson to serialize Instant fields correctly. Without it, you'd get cryptic serialization errors.

One small consideration: RateLimitState writes to ./data/rate-limit-state.json on safeSaveState() calls (triggered by recordRateLimit). In CI or parallel test runs, this could cause flaky behavior if tests share the same working directory. You might want to consider:

  1. Mocking the file I/O
  2. Using a temp directory per test
  3. Or ensuring cleanup in @AfterEach

That said, for isolated test runs this works fine — just something to keep in mind as the test suite grows!

src/main/java/com/williamcallahan/javachat/service/ExternalServiceHealth.java (1)

384-389: Minor consideration: ordering of state updates in markHealthy()

The current order is:

  1. isHealthy.set(true)
  2. consecutiveFailures.set(0)
  3. currentBackoff = ...
  4. lastCheck = ...
  5. checkInProgress.set(false)

There's a brief window where isHealthy is true but checkInProgress is still true. This is benign because isHealthy(serviceName) returns early on line 130 when healthy, but if you ever add code that checks both flags, you might see slightly inconsistent reads.

Not a bug — just something to keep in mind if this class evolves!

Comment threadfrontend/vite.config.ts

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:5cb023cff5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…alServiceHealth
Move URL construction and request spec building inside try blocks so
pre-subscription failures reset checkInProgress via markUnhealthy.
Add status.reset() before verifyQdrantCollectionsAfterStartup to
prevent ApplicationReadyEvent from being blocked by in-flight check.
Inline anemic checkQdrantHealthAsync wrapper.
Replace inline string literals with SESSION_NOT_FOUND_MESSAGE,
SESSION_FOUND_MESSAGE, and SESSION_FOUND_EMPTY_MESSAGE constants
to comply with no-magic-literals rule.
…ctory
Move duplicated private metadataText methods from DocsIngestionService
and LocalDocsFileIngestionProcessor into DocumentFactory as a static
utility to eliminate DRY violation.
@WilliamAGH
WilliamAGH merged commit a369e90 into mainFeb 11, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancementNew feature or requestjavaPull requests that update java coderefactorCode refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@WilliamAGH