Skip to content

feat(seo): add Clicky analytics injection with config policy and lint fixes#25

Merged
WilliamAGH merged 10 commits intomainfrom
dev
Feb 14, 2026
Merged

feat(seo): add Clicky analytics injection with config policy and lint fixes#25
WilliamAGH merged 10 commits intomainfrom
dev

Conversation

@WilliamAGH
Copy link
Owner

@WilliamAGH WilliamAGH commented Feb 14, 2026

Summary

  • SPA routes tracked by Clicky analytics without a frontend rebuild — script injection happens server-side in the SEO-rendered HTML shell
  • Secrets and config source policy codified: secrets via .env/env vars only, non-secret defaults in Spring property files
  • Frontend type-aware lint diagnostics resolved to keep make lint clean

Clicky Analytics

  • Server-side injection of Clicky site ID initializer + async script loader into <head>
  • Disabled by default in dev profile; enabled in production
  • Config bound via @ConfigurationProperties in AppProperties.Clicky with startup validation
  • DOM mutation logic isolated in ClickyAnalyticsInjector — controller only delegates

Configuration

  • app.clicky.enabled / app.clicky.site-id in Spring properties
  • Site ID validated as digits-only at startup (rejects non-numeric values)
  • SeoController receives already-validated config through constructor injection

Verification

  • ./gradlew test — 242 tests pass (0 failures)
  • make lint — 0 errors, 0 warnings (oxlint, ast-grep, svelte-check, spotbugs, pmd)

Commits

  • 53bc987 docs: codify secrets and config source policy
  • cdae404 feat(seo): inject Clicky analytics into SPA shell
  • 9712df1 fix(frontend): satisfy type-aware lint diagnostics
  • 99ab4e2 refactor(seo): bind Clicky analytics via @ConfigurationProperties
  • 95b4a17 style: apply oxfmt and Spotless formatting across codebase
  • a98116f fix(seo): add error logging and extract Clicky initializer constant
  • 4607510 refactor(config): replace manual ASCII loop with Character.isDigit()
  • f8f816f refactor(seo): extract Clicky DOM injection into ClickyAnalyticsInjector
  • fce1142 refactor(seo): delegate Clicky analytics from SeoController to injector
  • c1fe8ab test(seo): add ClickyAnalyticsInjector to SeoControllerTest context

This repo deploys via Coolify containers (BuildKit secrets) and runs locally
with `.env`. Codify the rule that secrets never belong in tracked Spring
property/YAML files, while keeping local/dev ergonomics intact.

- Add [EV1] policy for secrets + env var handling (OpenAI/GitHub Models, Qdrant)
- Clarify [TL1e] secrets storage expectations
- Clarify [LM1c] that only provider connectivity comes from `.env`/env vars
Add optional Clicky analytics injection to the SEO-rendered SPA shell so SPA
routes are tracked without a frontend rebuild. This follows the standard
Clicky pattern (clicky_site_ids + async loader) and is disabled for local dev.

- Inject Clicky initializer + async script loader when enabled
- Remove any Clicky tags when disabled to avoid double-injection
- Validate app.clicky.site-id digits-only when enabled
- Enable Clicky by default in prod; disable in dev profile
Type-aware oxlint flagged two issues: unsafe stringification of unknown stream
failures (Object default "[object Object]") and unnecessary type assertions on
marked.parse() results. This keeps lint green without changing app behavior.

- Format unknown stream failure diagnostics safely (JSON/string-aware)
- Remove unnecessary `as string` casts on marked.parse() return values
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Clicky analytics integrated, controllable via a feature flag (off in dev, enabled in production).
  • Documentation

    • Rules clarified: secrets and sensitive credentials must come from environment variables; do not store them in properties/YAML.
  • Refactor

    • Markdown parsing made more defensive to safely handle empty or unexpected content.
  • Bug Fixes

    • Stream failure diagnostics improved for clearer, consistent messages.
  • Style / Tests / Chores

    • Widespread formatting, test refinements, dependency manifest cleanup, and minor config tidy-ups across the frontend.

Walkthrough

Adds Clicky analytics support and config to the backend; enforces secrets-from-environment guidance in AGENTS.md; hardens frontend markdown parsing and stream diagnostics; and applies large-scale frontend stylistic, typing, and test adjustments.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md
Adds EV1 rules for Secrets & Env Vars and updates tooling rules (TL1e, LM1c) to require secrets (API keys, model base-urls) be provided from .env/environment variables, not properties/YAML.
Frontend — Markdown & Stream Recovery
frontend/src/lib/services/markdown.ts, frontend/src/lib/services/streamRecovery.ts, frontend/src/lib/services/streamRecovery.test.ts
parseMarkdown now accepts `string
Backend — Analytics & Config
src/main/java/.../web/SeoController.java, src/main/java/.../config/AppProperties.java, src/main/java/.../web/ClickyAnalyticsInjector.java, src/main/resources/application*.properties
Adds Clicky config block to AppProperties, new ClickyAnalyticsInjector component, wires injector into SeoController constructor, and adds app.clicky.* properties (enabled, site-id) with dev default disabled.
Frontend — Manifest & Build
frontend/package.json, frontend/vite.config.ts, frontend/svelte.config.js, frontend/vitest.config.ts
Reorganizes/declares dependencies (dompurify, highlight.js, marked, types, etc.), adds engines entry, and applies stylistic edits to build/config files.
Frontend — Bulk formatting, typing & tests
frontend/src/..., frontend/index.html, frontend/src/styles/global.css, frontend/src/test/setup.ts
Widespread stylistic normalization (quote style, semicolons, trailing commas), minor typing/annotation fixes, test refinements, expanded test coverage (streamRecovery, URL utils), and small mock enhancements.
Utilities & Small exports
frontend/src/lib/.../utils/*, .../services/*, .../composables/*, .../components/*
Numerous small formatting/consistency updates; a few public exports adjusted (e.g., toStreamFailureException, getDisplaySource) and punctuation/typing refinements in interfaces.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser
    participant SeoController as SeoController
    participant AppProperties as AppProperties
    participant Document as index.html

    Browser->>SeoController: GET /
    SeoController->>AppProperties: read app.clicky.enabled, app.clicky.site-id
    SeoController->>Document: updateDocumentMetadata(...)
    SeoController->>Document: clickyAnalyticsInjector.applyTo(Document)
    alt app.clicky.enabled == true and loader missing
        Document-->>Browser: include Clicky initializer (inline script)
        Document-->>Browser: include Clicky external script tag
    else app.clicky.enabled == false
        Document-->>Browser: remove Clicky initializer and script tags if present
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

refactor

Poem

Clicky slips in quietly, scripts aligned, 🎈
Secrets tuck to envs where keys are signed, 🔐
Markdown now guards its tokens with care,
Streams report truths sorted, neat and fair,
Tests march in semicolons — tidy and kind. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (41 files):

⚔️ AGENTS.md (content)
⚔️ frontend/config/oxlintrc.json (content)
⚔️ frontend/index.html (content)
⚔️ frontend/package.json (content)
⚔️ frontend/src/lib/components/ChatView.test.ts (content)
⚔️ frontend/src/lib/components/LearnView.test.ts (content)
⚔️ frontend/src/lib/composables/createScrollAnchor.svelte.ts (content)
⚔️ frontend/src/lib/composables/createStreamingState.svelte.ts (content)
⚔️ frontend/src/lib/services/chat.test.ts (content)
⚔️ frontend/src/lib/services/chat.ts (content)
⚔️ frontend/src/lib/services/csrf.test.ts (content)
⚔️ frontend/src/lib/services/guided.test.ts (content)
⚔️ frontend/src/lib/services/guided.ts (content)
⚔️ frontend/src/lib/services/markdown.test.ts (content)
⚔️ frontend/src/lib/services/markdown.ts (content)
⚔️ frontend/src/lib/services/sse.test.ts (content)
⚔️ frontend/src/lib/services/sse.ts (content)
⚔️ frontend/src/lib/services/streamRecovery.test.ts (content)
⚔️ frontend/src/lib/services/streamRecovery.ts (content)
⚔️ frontend/src/lib/stores/toastStore.ts (content)
⚔️ frontend/src/lib/utils/chatMessageId.ts (content)
⚔️ frontend/src/lib/utils/highlight.ts (content)
⚔️ frontend/src/lib/utils/scroll.test.ts (content)
⚔️ frontend/src/lib/utils/scroll.ts (content)
⚔️ frontend/src/lib/utils/session.test.ts (content)
⚔️ frontend/src/lib/utils/session.ts (content)
⚔️ frontend/src/lib/utils/url.test.ts (content)
⚔️ frontend/src/lib/utils/url.ts (content)
⚔️ frontend/src/lib/validation/schemas.ts (content)
⚔️ frontend/src/lib/validation/validate.ts (content)
⚔️ frontend/src/main.ts (content)
⚔️ frontend/src/styles/global.css (content)
⚔️ frontend/src/test/setup.ts (content)
⚔️ frontend/svelte.config.js (content)
⚔️ frontend/vite.config.ts (content)
⚔️ frontend/vitest.config.ts (content)
⚔️ src/main/java/com/williamcallahan/javachat/config/AppProperties.java (content)
⚔️ src/main/java/com/williamcallahan/javachat/web/SeoController.java (content)
⚔️ src/main/resources/application-dev.properties (content)
⚔️ src/main/resources/application.properties (content)
⚔️ src/test/java/com/williamcallahan/javachat/web/SeoControllerTest.java (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: Clicky analytics injection, configuration policy codification, and frontend lint fixes—all accurately reflected in the changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering Clicky analytics server-side injection, configuration policy, frontend type fixes, and providing verification details.

✏️ 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
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch dev
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/main/java/com/williamcallahan/javachat/web/SeoController.java`:
- Around line 47-57: Replace the scattered `@Value` parameters in the
SeoController constructor with a single AppProperties.ClickyProperties instance:
add a nested ClickyProperties class inside AppProperties (with fields
enabled:boolean and siteId:String plus any validation/@PostConstruct checks),
bind it under app.clicky via `@ConfigurationProperties`, and then change the
SeoController constructor to accept AppProperties (or
AppProperties.ClickyProperties) instead of the two `@Value` params; update the
constructor to set this.clickyEnabled = clickyProps.isEnabled() and
this.clickySiteId = clickyProps.isEnabled() ?
parseClickySiteId(clickyProps.getSiteId()) : -1L and remove the old `@Value`
imports/usages so configuration is centralized and validated in AppProperties.

SeoController was injecting Clicky settings through ad-hoc @value annotations
and duplicating the site-ID parsing/validation logic locally. This moves the
configuration into AppProperties.Clicky, validated at startup alongside all
other app.* sections, so parsing runs once and SeoController receives an
already-validated value object through constructor injection.

- Add AppProperties.Clicky inner class with enabled/siteId/parsedSiteId fields
- Wire Clicky into requireConfiguredSection startup validation
- Replace @value injections in SeoController with AppProperties dependency
- Remove SeoController.parseClickySiteId (logic now lives in Clicky.validateConfiguration)
Run oxfmt to enforce the frontend's canonical formatting: double quotes,
trailing commas, statement semicolons, and consistent line wrapping across
TypeScript sources, tests, HTML, CSS, JSON config, and build configs. Spotless
also reformatted a few long lines in the Java files touched by the previous
commit. No behavioral changes — only whitespace and punctuation normalization.
@coderabbitai coderabbitai bot added documentation Improvements or additions to documentation enhancement New feature or request java Pull requests that update java code labels Feb 14, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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/web/SeoController.java (1)

118-170: 🛠️ Refactor suggestion | 🟠 Major

Rename doc to document for intent-revealing naming.

Since this section was touched, align the parameter name with the naming rules for clarity. Tiny rename, big readability win.

✏️ Suggested rename
-    private void updateDocumentMetadata(Document doc, PageMetadata metadata, String fullUrl, String imageUrl) {
+    private void updateDocumentMetadata(Document document, PageMetadata metadata, String fullUrl, String imageUrl) {
@@
-        updateClickyAnalytics(doc);
+        updateClickyAnalytics(document);
     }
 
-    private void updateClickyAnalytics(Document doc) {
-        Element existingClickyLoader = doc.head().selectFirst("script[src=\"" + CLICKY_SCRIPT_URL + "\"]");
+    private void updateClickyAnalytics(Document document) {
+        Element existingClickyLoader = document.head().selectFirst("script[src=\"" + CLICKY_SCRIPT_URL + "\"]");
@@
-            doc.head().select("script").forEach(scriptTag -> {
+            document.head().select("script").forEach(scriptTag -> {
@@
-        doc.head().appendElement("script").text(initializer);
-        doc.head().appendElement("script").attr("async", "").attr("src", CLICKY_SCRIPT_URL);
+        document.head().appendElement("script").text(initializer);
+        document.head().appendElement("script").attr("async", "").attr("src", CLICKY_SCRIPT_URL);
     }

As per coding guidelines, “Banned abbreviations: doc … Every identifier must be domain-specific and intent-revealing.”

src/main/java/com/williamcallahan/javachat/config/AppProperties.java (1)

108-310: 🛠️ Refactor suggestion | 🟠 Major

Add Javadocs for the new public Clicky accessors.

Public methods in AppProperties.Clicky (and getClicky / setClicky) need Javadocs to satisfy the project’s documentation rule. Small docs now prevent future guesswork.

As per coding guidelines, “Javadocs are mandatory on public/protected classes, methods, and enums.”

🤖 Fix all issues with AI agents
In `@frontend/src/lib/services/markdown.ts`:
- Around line 376-401: parseMarkdown currently calls DOMPurify.sanitize(rawHtml,
...) directly which will throw during SSR because DOMPurify needs a DOM; make
parseMarkdown SSR-safe by replacing the direct DOMPurify.sanitize call with an
SSR-compatible initialization: either switch to using the isomorphic-dompurify
package and call its sanitize method, or create a DOMPurify instance bound to a
jsdom window before sanitizing (initialize once and reuse), and then call that
instance's sanitize with the same options; update references in parseMarkdown
(and any helper usages such as the marked.parse → rawHtml flow) so
DOMPurify.sanitize is only invoked via the SSR-safe instance.

In `@frontend/src/styles/global.css`:
- Around line 51-66: Remove the stylelint failures by unquoting the single-word
family name and normalizing the system font casing: in the `@font-face` block and
the --font-serif variable remove the surrounding quotes from Fraunces (use
Fraunces without quotes) and adjust the font stack in --font-sans to use the
expected BlinkMacSystemFont token/casing that your linter expects (ensure the
BlinkMacSystemFont entry matches the project's canonical casing/token), then run
the linter to confirm the errors are gone.
🧹 Nitpick comments (1)
frontend/package.json (1)

23-36: Consider moving @types/dompurify to devDependencies.
Tiny tidbit: @types/* packages are compile‑time only, so keeping them in devDependencies trims production installs.

♻️ Suggested tweak
   "dependencies": {
-    "@types/dompurify": "^3.0.5",
     "dompurify": "^3.3.1",
     "highlight.js": "^11.10.0",
     "marked": "^15.0.0",
     "zod": "^3.25.76"
   },
   "devDependencies": {
+    "@types/dompurify": "^3.0.5",
     "@sveltejs/vite-plugin-svelte": "^5.0.0",

SeoController's IOException handler silently discarded the exception with no
log statement, making production failures invisible to operators. Additionally,
the Clicky JavaScript initializer was an inline magic string rather than a
named constant, reducing readability and violating naming discipline.

- Add SLF4J Logger to SeoController for operational visibility
- Log IOException with full stack trace before returning 500 response
- Extract CLICKY_INITIALIZER_TEMPLATE constant for the site ID push script
- Use String.format() instead of string concatenation for the initializer
Clicky site ID validation used a manual char-by-char loop with raw ASCII
ordinal comparisons (character < '0' || character > '9'), which is harder to
read than the idiomatic Java alternative. Replace with a single-expression
stream using Character.isDigit() for clarity while preserving identical
validation behavior — Long.parseLong() on the next line still rejects any
non-ASCII digits that isDigit() might accept.

- Replace 6-line manual loop with trimmedSiteId.chars().allMatch(Character::isDigit)
- Extract boolean allDigits for self-documenting conditional
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/main/java/com/williamcallahan/javachat/web/SeoController.java`:
- Around line 151-176: The Clicky DOM mutation logic inside
SeoController.updateClickyAnalytics should be moved into a new
single-responsibility class (e.g., ClickyAnalyticsInjector) that encapsulates
detection/removal/insertion using CLICKY_SCRIPT_URL and
CLICKY_INITIALIZER_TEMPLATE and reads the clickyEnabled and clickySiteId flags;
create ClickyAnalyticsInjector with a public method injectIfEnabled(Document)
(or removeIfDisabled(Document)), have SeoController call that method from
updateClickyAnalytics (or replace updateClickyAnalytics with a one-line
delegate), and ensure the new class owns all Jsoup DOM changes so the controller
only delegates and contains no analytics DOM logic.
🧹 Nitpick comments (1)
src/main/java/com/williamcallahan/javachat/web/SeoController.java (1)

156-176: Replace new magic string literals with named constants.
The new selector/token/tag/attribute strings should be constants to align with the no‑magic‑literals rule.

🔧 Example refactor (constants)
+    private static final String CLICKY_LOADER_SELECTOR =
+            "script[src=\"" + CLICKY_SCRIPT_URL + "\"]";
+    private static final String CLICKY_SITE_IDS_TOKEN = "clicky_site_ids";
+    private static final String CLICKY_SCRIPT_TAG = "script";
+    private static final String CLICKY_SCRIPT_ATTR_ASYNC = "async";
+    private static final String CLICKY_SCRIPT_ATTR_SRC = "src";
...
-        Element existingClickyLoader = doc.head().selectFirst("script[src=\"" + CLICKY_SCRIPT_URL + "\"]");
+        Element existingClickyLoader = doc.head().selectFirst(CLICKY_LOADER_SELECTOR);
...
-            doc.head().select("script").forEach(scriptTag -> {
+            doc.head().select(CLICKY_SCRIPT_TAG).forEach(scriptTag -> {
                 String scriptBody = scriptTag.html();
-                if (scriptBody != null && scriptBody.contains("clicky_site_ids")) {
+                if (scriptBody != null && scriptBody.contains(CLICKY_SITE_IDS_TOKEN)) {
                     scriptTag.remove();
                 }
             });
...
-        doc.head().appendElement("script").attr("async", "").attr("src", CLICKY_SCRIPT_URL);
+        doc.head()
+                .appendElement(CLICKY_SCRIPT_TAG)
+                .attr(CLICKY_SCRIPT_ATTR_ASYNC, "")
+                .attr(CLICKY_SCRIPT_ATTR_SRC, CLICKY_SCRIPT_URL);

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

SeoController was performing Clicky script detection, removal, and insertion
directly in its updateClickyAnalytics method, mixing analytics DOM mutation
with HTTP-to-HTML translation. This violates controller single responsibility
(adapters/in/web should only delegate, not own business logic).

- Create ClickyAnalyticsInjector @component with applyTo(Document) method
- Move CLICKY_SCRIPT_URL and CLICKY_INITIALIZER_TEMPLATE constants
- Move clickyEnabled/clickySiteId fields and AppProperties.Clicky wiring
- Extract removeClickyTags private helper for disabled-state cleanup
SeoController now delegates all Clicky DOM mutations to the new
ClickyAnalyticsInjector component via a single applyTo(doc) call,
removing the updateClickyAnalytics method and its analytics-specific
constants/fields from the controller.

- Replace updateClickyAnalytics with clickyAnalyticsInjector.applyTo(doc)
- Remove CLICKY_SCRIPT_URL, CLICKY_INITIALIZER_TEMPLATE constants
- Remove clickyEnabled, clickySiteId fields
- Replace AppProperties constructor dependency with ClickyAnalyticsInjector
SeoControllerTest uses @WebMvcTest which only loads the controller slice.
After extracting ClickyAnalyticsInjector from SeoController, the test
context needs the new component imported alongside AppProperties.

- Add ClickyAnalyticsInjector.class to @import annotation
@WilliamAGH WilliamAGH changed the title dev -> main: Clicky analytics + config policy + lint fixes feat(seo): add Clicky analytics injection with config policy and lint fixes Feb 14, 2026
@coderabbitai coderabbitai bot added the refactor Code refactoring label Feb 14, 2026
@WilliamAGH WilliamAGH merged commit aa3bac1 into main Feb 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request java Pull requests that update java code refactor Code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments