Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ jobs:
env:
CI: true
run: pnpm --filter cwv-monitor-app test:perf

- name: Run monitor anomaly detection tests
if: steps.changes.outputs.monitor_changed == 'true'
env:
CI: true
run: pnpm --filter cwv-monitor-app test:anomaly

# SDK can destroy client build, so let's verify it here too
- name: Check demo (Build)
Expand Down
20 changes: 20 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
```
apps/
monitor-app # Next.js dashboard & API
anomaly-worker # Sidecar service for anomaly detection (new)

packages/
client-sdk # Browser SDK for CWV collection
Expand All @@ -22,15 +23,24 @@ flowchart LR
Ingest["POST /api/ingest<br>schema + rate limit"]
Dashboard["Dashboard UI + auth"]
end
subgraph Worker["Anomaly Worker"]
Poller["Poller (node-cron)"]
Notifier["Slack/Teams Notifier"]
end
subgraph CH["ClickHouse"]
Raw["cwv_events & custom_events<br>MergeTree, TTL 90d"]
Agg["cwv_daily_aggregates<br>AggregatingMergeTree, 365d"]
Anom["v_cwv_anomalies (View)<br>Z-Score logic"]
end
SDK -->|"batched payload"| Ingest
Ingest -->|"validated & enriched"| Raw
Raw -->|"mv_cwv_daily_aggregates"| Agg
Dashboard -->|"analytics queries"| Agg
Dashboard -->|"drill-down"| Raw
Poller -->|"polls every hour"| Anom
Poller -->|"fetches project info"| Raw
Poller -->|"marks as notified"| Raw
Poller -->|"sends notifications"| Notifier
Comment on lines +40 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Diagram target for notification state is inconsistent with the text.

Line 42 shows “marks as notified” going to Raw, but Line 93 documents processed_anomalies as the state store. Please align the diagram with the documented table.

Also applies to: 93-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ARCHITECTURE.md` around lines 40 - 43, The diagram currently points the
"marks as notified" (and the other notification-state arrow) to the Raw node;
update the diagram so the Poller arrows that represent notification state
mutations ("marks as notified") target the processed_anomalies state store
instead of Raw (i.e., replace Raw with processed_anomalies for those arrows),
and ensure Poller -->|"fetches project info"| still targets the correct node if
it should also be processed_anomalies; update any duplicate arrow entries (the
ones referenced around lines 42 and 93-94) so diagram labels and targets match
the documented processed_anomalies state.

```

```mermaid
Expand Down Expand Up @@ -74,6 +84,16 @@ Lightweight browser SDK that collects CWV metrics (LCP, INP, CLS, TTFB, FCP), `$

Shared TypeScript schemas for ingest payloads. Imported by both SDK and monitor app to prevent drift.

### 3.4. Anomaly Worker (`apps/anomaly-worker`)

A lightweight sidecar service that periodically polls ClickHouse for statistical anomalies (z_score > 3) and sends notifications.

- **Stack:** Node.js, node-cron, ClickHouse client, pino
- **Polling:** Scheduled every hour to detect new regressions
- **State:** Tracks notified anomalies in `processed_anomalies` table to prevent duplicates
- **Notifications:** Supports Slack and Microsoft Teams webhooks
- **Deployment:** Dedicated sidecar container (see `docker/anomaly-worker.Dockerfile`)

## 4. Data Store

**ClickHouse** — High-performance columnar database for analytics.
Expand Down
20 changes: 20 additions & 0 deletions apps/anomaly-worker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ClickHouse connection details
CLICKHOUSE_HOST=localhost
CLICKHOUSE_PORT=8123
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=secret
CLICKHOUSE_DB=cwv_monitor
AI_ANALYST_CLICKHOUSE_PASSWORD=
BETTER_AUTH_SECRET=
INITIAL_USER_EMAIL=
INITIAL_USER_PASSWORD=
INITIAL_USER_NAME=

# Application
LOG_LEVEL=info
NODE_ENV=development
AUTH_BASE_URL=http://localhost:3000

# Anomaly Detection Webhooks (Optional)
SLACK_WEBHOOK_URL=
TEAMS_WEBHOOK_URL=
Comment on lines +1 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "apps/anomaly-worker/\.env"

Repository: Blazity/next-cwv-monitor

Length of output: 125


🏁 Script executed:

if [ -f "apps/anomaly-worker/.env" ]; then echo "=== .env exists ==="; wc -l apps/anomaly-worker/.env; else echo ".env not found"; fi
if [ -f "apps/anomaly-worker/.env.example" ]; then echo "=== .env.example exists ==="; wc -l apps/anomaly-worker/.env.example; else echo ".env.example not found"; fi

Repository: Blazity/next-cwv-monitor

Length of output: 179


🏁 Script executed:

if [ -f "apps/anomaly-worker/.env" ] && [ -f "apps/anomaly-worker/.env.example" ]; then
  echo "=== Content of .env ==="; cat -n apps/anomaly-worker/.env
  echo
  echo "=== Content of .env.example ==="; cat -n apps/anomaly-worker/.env.example
fi

Repository: Blazity/next-cwv-monitor

Length of output: 1156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ ! -f "apps/anomaly-worker/.env" ] || [ ! -f "apps/anomaly-worker/.env.example" ]; then
  echo "One or both files not found"
  exit 1
fi

echo "Keys in apps/anomaly-worker/.env but missing in apps/anomaly-worker/.env.example:"
comm -23 \
  <(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort) \
  <(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort)

echo
echo "Keys in .env.example but missing in .env:"
comm -13 \
  <(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort) \
  <(grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort)

Repository: Blazity/next-cwv-monitor

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Keys in .env ==="
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort

echo
echo "=== Keys in .env.example ==="
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort

echo
echo "=== Missing from .env.example (in .env but not in example) ==="
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env | cut -d= -f1 | sort > /tmp/env_keys.txt
grep -E '^[A-Z0-9_]+=' apps/anomaly-worker/.env.example | cut -d= -f1 | sort > /tmp/env_example_keys.txt
comm -23 /tmp/env_keys.txt /tmp/env_example_keys.txt

Repository: Blazity/next-cwv-monitor

Length of output: 666


🏁 Script executed:

# Check if missing variables are required at runtime or have defaults in the code
cd apps/anomaly-worker && (
  echo "=== Checking references to missing variables ==="
  for var in AI_ANALYST_CLICKHOUSE_PASSWORD BETTER_AUTH_SECRET INITIAL_USER_EMAIL INITIAL_USER_NAME INITIAL_USER_PASSWORD; do
    echo "Searching for $var..."
    rg "$var" --type ts --type js --type json 2>/dev/null | head -5 || echo "  Not found in code"
  done
)

Repository: Blazity/next-cwv-monitor

Length of output: 942


.env.example missing required bootstrap variables.

The template omits 5 variables present in .env: AI_ANALYST_CLICKHOUSE_PASSWORD, BETTER_AUTH_SECRET, INITIAL_USER_EMAIL, INITIAL_USER_NAME, and INITIAL_USER_PASSWORD. These are validated as required in src/env.ts with strict validation rules (min length, email format). Developers copying only .env.example during fresh setup will encounter validation errors at runtime.

Required additions to .env.example
 # ClickHouse connection details
 CLICKHOUSE_HOST=localhost
 CLICKHOUSE_PORT=8123
 CLICKHOUSE_USER=default
 CLICKHOUSE_PASSWORD=secret
 CLICKHOUSE_DB=cwv_monitor
+AI_ANALYST_CLICKHOUSE_PASSWORD=
+BETTER_AUTH_SECRET=
+INITIAL_USER_EMAIL=
+INITIAL_USER_PASSWORD=
+INITIAL_USER_NAME=
 
 # Application
 LOG_LEVEL=info
 NODE_ENV=development
 AUTH_BASE_URL=http://localhost:3000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# ClickHouse connection details
CLICKHOUSE_HOST=localhost
CLICKHOUSE_PORT=8123
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=secret
CLICKHOUSE_DB=cwv_monitor
# Application
LOG_LEVEL=info
NODE_ENV=development
AUTH_BASE_URL=http://localhost:3000
# Anomaly Detection Webhooks (Optional)
SLACK_WEBHOOK_URL=
TEAMS_WEBHOOK_URL=
# ClickHouse connection details
CLICKHOUSE_HOST=localhost
CLICKHOUSE_PORT=8123
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=secret
CLICKHOUSE_DB=cwv_monitor
AI_ANALYST_CLICKHOUSE_PASSWORD=
BETTER_AUTH_SECRET=
INITIAL_USER_EMAIL=
INITIAL_USER_PASSWORD=
INITIAL_USER_NAME=
# Application
LOG_LEVEL=info
NODE_ENV=development
AUTH_BASE_URL=http://localhost:3000
# Anomaly Detection Webhooks (Optional)
SLACK_WEBHOOK_URL=
TEAMS_WEBHOOK_URL=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 5-5: [UnorderedKey] The CLICKHOUSE_PASSWORD key should go before the CLICKHOUSE_PORT key

(UnorderedKey)


[warning] 6-6: [UnorderedKey] The CLICKHOUSE_DB key should go before the CLICKHOUSE_HOST key

(UnorderedKey)


[warning] 11-11: [UnorderedKey] The AUTH_BASE_URL key should go before the LOG_LEVEL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/anomaly-worker/.env.example` around lines 1 - 15, The .env.example is
missing required bootstrap variables that are strictly validated in src/env.ts;
add the following entries to .env.example: AI_ANALYST_CLICKHOUSE_PASSWORD
(placeholder, non-empty), BETTER_AUTH_SECRET (placeholder, min-length matching
src/env.ts), INITIAL_USER_EMAIL (placeholder in valid email format),
INITIAL_USER_NAME (placeholder, non-empty), and INITIAL_USER_PASSWORD
(placeholder, min-length matching src/env.ts). Ensure the placeholder values
conform to the validation rules enforced in src/env.ts (email format for
INITIAL_USER_EMAIL and minimum lengths for secrets/passwords) so developers
copying .env.example won’t fail validation at startup.

1 change: 1 addition & 0 deletions apps/anomaly-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
41 changes: 41 additions & 0 deletions apps/anomaly-worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "anomaly-worker",
"version": "0.1.0",
"private": true,
"description": "A lightweight worker for detecting anomalies.",
"main": "dist/anomaly-worker/src/index.js",
"type": "module",
"scripts": {
"prebuild": "rm -rf dist",
"build": "tsc",
"start": "cross-env NODE_OPTIONS=\"-r dotenv/config\" tsx src/index.ts",
"dev": "tsx src/index.ts | pino-pretty",
"test": "vitest",
"test:watch": "vitest --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@clickhouse/client": "^1.14.0",
"@t3-oss/env-nextjs": "^0.13.8",
"arktype": "^2.1.28",
"date-fns": "^4.1.0",
"node-cron": "^3.0.3",
"pino": "^10.1.0",
"remeda": "^2.32.0",
"waddler": "^0.1.1",
"zod": "^4.1.13",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@types/node": "^24",
"@types/node-cron": "^3.0.11",
"pino-pretty": "^13.0.0",
"tsx": "^4.19.2",
"typescript": "^5.9.3",
"vitest": "^4.0.15",
"tsconfig-paths": "^4.2.0",
"cross-env": "^7.0.3"
}
}
54 changes: 54 additions & 0 deletions apps/anomaly-worker/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

const LOG_LEVELS = ["fatal", "error", "warn", "info", "debug", "trace", "silent"] as const;

export const env = createEnv({
server: {
AUTH_BASE_URL: z.url(),
TRUST_PROXY: z.enum(["true", "false"]).default("false"),
CLICKHOUSE_HOST: z.string().min(1, "CLICKHOUSE_HOST is required"),
CLICKHOUSE_PORT: z.string().min(1, "CLICKHOUSE_PORT is required"),
CLICKHOUSE_USER: z.string().min(1, "CLICKHOUSE_USER is required"),
CLICKHOUSE_PASSWORD: z.string(),
CLICKHOUSE_DB: z.string().min(1, "CLICKHOUSE_DB is required"),
AI_ANALYST_CLICKHOUSE_USER: z.string().min(1).default("ai_analyst_user"),
AI_ANALYST_CLICKHOUSE_PASSWORD: z.string().min(1),
BETTER_AUTH_SECRET: z.string(),
CLICKHOUSE_ADAPTER_DEBUG_LOGS: z.coerce.boolean().default(false),
MIN_PASSWORD_SCORE: z.coerce.number().min(0).max(4).default(2),
RATE_LIMIT_WINDOW_MS: z.coerce.number().positive().default(60_000),
MAX_LOGIN_ATTEMPTS: z.coerce.number().positive().default(5),
INITIAL_USER_EMAIL: z.email(),
INITIAL_USER_PASSWORD: z.string().min(8),
INITIAL_USER_NAME: z.string().min(3),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
LOG_LEVEL: z.enum(LOG_LEVELS).default("info"),
SLACK_WEBHOOK_URL: z.url().optional(),
TEAMS_WEBHOOK_URL: z.url().optional(),
},
client: {},
runtimeEnv: {
AUTH_BASE_URL: process.env.AUTH_BASE_URL,
TRUST_PROXY: process.env.TRUST_PROXY,
CLICKHOUSE_HOST: process.env.CLICKHOUSE_HOST,
CLICKHOUSE_PORT: process.env.CLICKHOUSE_PORT,
CLICKHOUSE_USER: process.env.CLICKHOUSE_USER,
CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,
CLICKHOUSE_DB: process.env.CLICKHOUSE_DB,
AI_ANALYST_CLICKHOUSE_USER: process.env.AI_ANALYST_CLICKHOUSE_USER,
AI_ANALYST_CLICKHOUSE_PASSWORD: process.env.AI_ANALYST_CLICKHOUSE_PASSWORD,
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
CLICKHOUSE_ADAPTER_DEBUG_LOGS: process.env.CLICKHOUSE_ADAPTER_DEBUG_LOGS,
MIN_PASSWORD_SCORE: process.env.MIN_PASSWORD_SCORE,
RATE_LIMIT_WINDOW_MS: process.env.RATE_LIMIT_WINDOW_MS,
MAX_LOGIN_ATTEMPTS: process.env.MAX_LOGIN_ATTEMPTS,
INITIAL_USER_EMAIL: process.env.INITIAL_USER_EMAIL,
INITIAL_USER_PASSWORD: process.env.INITIAL_USER_PASSWORD,
INITIAL_USER_NAME: process.env.INITIAL_USER_NAME,
NODE_ENV: process.env.NODE_ENV,
LOG_LEVEL: process.env.LOG_LEVEL,
SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL,
TEAMS_WEBHOOK_URL: process.env.TEAMS_WEBHOOK_URL,
},
});
57 changes: 57 additions & 0 deletions apps/anomaly-worker/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pino from 'pino';
import cron from 'node-cron';
import { env } from "./env";

const logger = pino({ name: 'anomaly-worker', level: env.LOG_LEVEL });

let isRunning = false;
let isShuttingDown = false;

async function runDetectionCycle() {
if (isRunning || isShuttingDown) {
return;
}

isRunning = true;
logger.info("Starting anomaly detection cycle...");

try {
const { NotificationsService } = await import('@monitor-app/app/server/domain/notifications/service');
const service = new NotificationsService();

await service.notifyNewAnomalies();
logger.info("Anomaly detection cycle finished.");
} catch (error) {
logger.error({ err: error }, "Error during detection cycle.");
} finally {
isRunning = false;
}
}

const task = cron.schedule('30 * * * *', runDetectionCycle);
logger.info("Anomaly worker scheduled (30 * * * *).");

runDetectionCycle();

process.on('SIGTERM', async () => {
isShuttingDown = true;
task.stop();

if (isRunning) {
logger.info("Waiting for the current detection cycle to complete...");

const shutdownTimeout = setTimeout(() => {
logger.error("Shutdown timed out; forcing exit.");
process.exit(1);
}, 20000);

while (isRunning) {
await new Promise(resolve => setTimeout(resolve, 500));
}

clearTimeout(shutdownTimeout);
}

logger.info("Shutdown complete.");
process.exit(0);
});
18 changes: 18 additions & 0 deletions apps/anomaly-worker/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["./src/*", "../../apps/monitor-app/src/*"],
"@monitor-app/*": ["../monitor-app/src/*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
2 changes: 2 additions & 0 deletions apps/monitor-app/.env.ci
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ CLICKHOUSE_PORT=18123
CLICKHOUSE_DB=cwv_monitor_test
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=secret
AI_ANALYST_CLICKHOUSE_USER=ai_analyst_user
AI_ANALYST_CLICKHOUSE_PASSWORD=ai_analyst_password

# Auth (required by `src/env.ts`)
BETTER_AUTH_SECRET=ci-test-secret-that-is-at-least-32-chars-long
Expand Down
6 changes: 6 additions & 0 deletions apps/monitor-app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ CLICKHOUSE_PORT=8123
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=secret
CLICKHOUSE_DB=cwv_monitor
AI_ANALYST_CLICKHOUSE_USER=ai_analyst_user
AI_ANALYST_CLICKHOUSE_PASSWORD=ai_analyst_password

# Optional overrides for the clickhouse-migrations CLI
CH_MIGRATIONS_HOST=${CLICKHOUSE_HOST}
Expand All @@ -23,3 +25,7 @@ INITIAL_USER_PASSWORD=password
INITIAL_USER_NAME=User

MIN_PASSWORD_SCORE=2

# Anomaly Detection Webhooks (Optional)
# SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
# TEAMS_WEBHOOK_URL=https://outlook.office.com/webhook/...
3 changes: 3 additions & 0 deletions apps/monitor-app/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ CLICKHOUSE_PORT=18123
CLICKHOUSE_DB=cwv_monitor_test
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=secret
AI_ANALYST_CLICKHOUSE_USER=ai_analyst_user
AI_ANALYST_CLICKHOUSE_PASSWORD=ai_analyst_password

CH_MIGRATIONS_HOST=localhost
CH_MIGRATIONS_PORT=8123
Expand All @@ -17,5 +19,6 @@ INITIAL_USER_EMAIL=initial@example.com
INITIAL_USER_PASSWORD=password1234
INITIAL_USER_NAME=Initial User


# Logging
LOG_LEVEL=debug
88 changes: 88 additions & 0 deletions apps/monitor-app/AI_AGENT_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# AI Agent Architecture & Integration Guide

This document outlines the bidirectional AI monitoring system for the CWV Monitor. It explains how the **Monitor App** acts as the analytical engine for **AI Agent (Slack/Teams Bot)**.

## 1. System Components

### Monitor App (`apps/monitor-app`)
The "Source of Truth" and Data Engine.
- **ClickHouse**: Stores all raw performance events and pre-aggregated metrics.
- **AiBridge**: A domain service that provides safe, high-level methods for the AI Agent to query data.
- **Schema Catalog**: A YAML definition (`clickhouse/catalog.yml`) that describes the database to the LLM.

### AI Agent
The interaction layer (Slack/Teams).
- **Inbound Handling**: Receives webhooks and message events from Slack/Teams.
- **LLM Brain**: Uses the `AiBridge` from the Monitor App to fetch context and execute diagnostic SQL.
- **Conversation State**: Tracks thread IDs and maps them to specific `anomaly_id`s.

### Anomaly Worker (`apps/anomaly-worker`)
The proactive trigger.
- Runs on a cron schedule.
- Scans ClickHouse for statistical anomalies (Z-Score > 3).
- Triggers the initial outbound notification via the Monitor App's `NotificationsService`.

---

## 2. Security Model

We follow the principle of **Least Privilege** for AI interactions:

- **Restricted User**: The Agent uses `ai_analyst_user` in ClickHouse.
- **Role-Based Access**: The `r_ai_analyst` role is limited to `SELECT` on core tables and `INSERT/UPDATE` only on the `processed_anomalies` audit table.
- **Resource Guardrails**: The `ai_analyst_profile` sets hard limits on memory (2GB), execution time (15s), and rows read (100M) to prevent the LLM from generating "expensive" or runaway queries.

---

## 3. Capabilities ("What can be done")

### Outbound Alerts
When an anomaly is detected, the `NotificationsService` sends a rich payload to Slack/Teams:
- **Investigate Button**: Links to the Monitor App Web UI.
- **Chat with AI Button**: Deep-links to the AI Agent to start a diagnostic conversation.
- **Metadata**: Includes `anomalyId`, `projectId`, and `metricName` so the Agent knows exactly what to talk about.

### Deep-Dive Context
The AI Agent can call `aiBridge.getAnomalyContext(anomalyId)` to receive a "Context Bundle":
1. **Anomaly Record**: Specifics about the z-score and metric.
2. **Project Info**: Domain and configuration.
3. **24h Trend**: Pre-fetched hourly averages for the last 24 hours.
4. **Schema Reference**: The `catalog.yml` content to help the LLM generate its own SQL.

### Dynamic SQL Execution
The Agent can ask the LLM to "Verify if this happens on other routes." The LLM generates a SQL query, and the Agent executes it via `aiBridge.executeSql(query, params)`.

---

## 4. Integration Pattern

To use the Monitor App domain in your AI application:

```typescript
import { aiBridge } from "@monitor-app/app/server/domain/ai/service";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incorrect import path in documentation.

The import path references @monitor-app/app/server/domain/ai/service, but based on the PR changes, the actual service is located at @monitor-app/app/server/domain/ai-bridge/service. This should be updated to match the actual file structure.

📝 Proposed fix
-import { aiBridge } from "@monitor-app/app/server/domain/ai/service";
+import { aiBridge } from "@monitor-app/app/server/domain/ai-bridge/service";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { aiBridge } from "@monitor-app/app/server/domain/ai/service";
import { aiBridge } from "@monitor-app/app/server/domain/ai-bridge/service";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/monitor-app/AI_AGENT_GUIDE.md` at line 62, The documentation import is
pointing to the wrong module: update the import that brings in aiBridge so it
references the actual service module (use the ai-bridge service module name)
instead of the old ai/service path; modify the import line that currently
imports aiBridge to import from the ai-bridge service module so it matches the
real export and builds correctly.


// 1. When a user clicks "Chat with AI" in Slack
async function handleChatStart(anomalyId: string) {
// Get everything the LLM needs to know in one call
const context = await aiBridge.getAnomalyContext(anomalyId);

// Provide this context to your LLM system prompt
const systemPrompt = `
You are a Performance Expert.
Analyze this anomaly: ${JSON.stringify(context.anomaly)}
Database Schema: ${context.schemaReference}
...
`;
}

// 2. When the LLM wants to fetch more data
async function onLlmToolUse(generatedSql: string) {
const results = await aiBridge.executeSql(generatedSql);
return results;
}
```

## 5. Files Reference
- **Schema Catalog**: `apps/monitor-app/clickhouse/catalog.yml`
- **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai/service.ts`
- **Notification Logic**: `apps/monitor-app/src/app/server/domain/notifications/`
Comment on lines +85 to +88
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incorrect file path in reference.

The file reference should point to ai-bridge directory, not ai, to match the actual implementation.

📝 Proposed fix
 ## 5. Files Reference
 - **Schema Catalog**: `apps/monitor-app/clickhouse/catalog.yml`
-- **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai/service.ts`
+- **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai-bridge/service.ts`
 - **Notification Logic**: `apps/monitor-app/src/app/server/domain/notifications/`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 5. Files Reference
- **Schema Catalog**: `apps/monitor-app/clickhouse/catalog.yml`
- **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai/service.ts`
- **Notification Logic**: `apps/monitor-app/src/app/server/domain/notifications/`
## 5. Files Reference
- **Schema Catalog**: `apps/monitor-app/clickhouse/catalog.yml`
- **AI Domain Logic**: `apps/monitor-app/src/app/server/domain/ai-bridge/service.ts`
- **Notification Logic**: `apps/monitor-app/src/app/server/domain/notifications/`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/monitor-app/AI_AGENT_GUIDE.md` around lines 85 - 88, Update the "AI
Domain Logic" reference in AI_AGENT_GUIDE.md to point to the ai-bridge
implementation instead of the ai one: locate the "AI Domain Logic" entry (the
line that currently references service.ts) and change its referenced directory
from ai to ai-bridge so it correctly points at the actual implementation file
(service.ts) under the ai-bridge module.

Loading