fix: insights-docs button contrast + dark mode + missing product pages

- Force dark mode by default (html class="dark")
- Fix global.css: use --color-fd-primary (not --fd-primary) for Tailwind v4
- Fix all primary buttons: text-white → text-fd-primary-foreground (auto-inverts)
- Fix secondary buttons: add explicit text-fd-foreground
- Add 4 missing product pages: replay, experiments, surveys, llm-analytics
- 13 routes now (was 9), all fully implemented with code examples
This commit is contained in:
2026-02-28 13:13:23 -08:00
parent 5815ee6cf7
commit b29ba25e45
9 changed files with 429 additions and 29 deletions
+7 -7
View File
@@ -46,21 +46,21 @@ export default function HomePage() {
href="https://app.insights.hanzo.ai"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-6 py-3 text-sm font-medium text-white hover:opacity-90 transition"
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-6 py-3 text-sm font-medium text-fd-primary-foreground hover:opacity-90 transition"
>
<Activity className="h-4 w-4" />
Start Free
</a>
<Link
href="/docs"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-6 py-3 text-sm font-medium hover:bg-fd-muted transition"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-6 py-3 text-sm font-medium text-fd-foreground hover:bg-fd-muted transition"
>
<BookOpen className="h-4 w-4" />
Documentation
</Link>
<Link
href="/docs/api"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-6 py-3 text-sm font-medium hover:bg-fd-muted transition"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-6 py-3 text-sm font-medium text-fd-foreground hover:bg-fd-muted transition"
>
<Terminal className="h-4 w-4" />
API Reference
@@ -69,7 +69,7 @@ export default function HomePage() {
href="https://github.com/hanzoai/insights"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-6 py-3 text-sm font-medium hover:bg-fd-muted transition"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-6 py-3 text-sm font-medium text-fd-foreground hover:bg-fd-muted transition"
>
<Github className="h-4 w-4" />
GitHub
@@ -465,14 +465,14 @@ kubectl apply -k \\
href="https://app.insights.hanzo.ai"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-8 py-3 text-sm font-medium text-white hover:opacity-90 transition"
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-8 py-3 text-sm font-medium text-fd-primary-foreground hover:opacity-90 transition"
>
<Activity className="h-4 w-4" />
Open Dashboard
</a>
<Link
href="/docs"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-8 py-3 text-sm font-medium hover:bg-fd-muted transition"
className="inline-flex items-center gap-2 rounded-lg border border-fd-border bg-fd-background px-8 py-3 text-sm font-medium text-fd-foreground hover:bg-fd-muted transition"
>
<BookOpen className="h-4 w-4" />
Read the Docs
@@ -536,7 +536,7 @@ kubectl apply -k \\
href="https://app.insights.hanzo.ai"
target="_blank"
rel="noopener noreferrer"
className="fixed bottom-6 left-6 z-50 flex items-center gap-2 rounded-full bg-fd-primary px-4 py-2.5 text-sm font-medium text-white shadow-lg hover:opacity-90 transition"
className="fixed bottom-6 left-6 z-50 flex items-center gap-2 rounded-full bg-fd-primary px-4 py-2.5 text-sm font-medium text-fd-primary-foreground shadow-lg hover:opacity-90 transition"
title="Open Hanzo Insights dashboard"
>
<Activity className="h-4 w-4" />
+10 -16
View File
@@ -2,22 +2,16 @@
@import '@hanzo/docs-base-ui/css/neutral.css';
@import '@hanzo/docs-base-ui/css/preset.css';
:root {
--fd-primary: 234 88 12; /* orange-600 */
--fd-background: 255 255 255; /* white */
--fd-foreground: 15 23 42; /* slate-900 */
--fd-muted: 241 245 249; /* slate-100 */
--fd-muted-foreground: 100 116 139; /* slate-500 */
--fd-accent: 255 237 213; /* orange-100 */
--fd-accent-foreground: 124 45 18; /* orange-900 */
}
/* ── Hanzo Insights theme: monochrome dark-first ────────────────── */
/* Default-colors.css already provides: */
/* light: --color-fd-primary: near-black (dark buttons) */
/* dark: --color-fd-primary: near-white (light buttons) */
/* We only need to tweak backgrounds for deeper blacks in dark mode */
.dark {
--fd-primary: 249 115 22; /* orange-500 */
--fd-background: 10 10 10; /* near-black */
--fd-foreground: 241 245 249; /* slate-100 */
--fd-muted: 30 41 59; /* slate-800 */
--fd-muted-foreground: 148 163 184; /* slate-400 */
--fd-accent: 67 20 7; /* orange-950 */
--fd-accent-foreground: 255 237 213; /* orange-100 */
--color-fd-background: hsl(0, 0%, 4%); /* #0a0a0a */
--color-fd-card: hsl(0, 0%, 7%);
--color-fd-popover: hsl(0, 0%, 9%);
--color-fd-muted: hsl(0, 0%, 11%);
--color-fd-border: hsla(0, 0%, 40%, 15%);
}
+1 -1
View File
@@ -22,7 +22,7 @@ export const viewport: Viewport = {
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<html lang="en" className="dark" suppressHydrationWarning>
<body className="antialiased">
<RootProvider>{children}</RootProvider>
<script defer src="https://analytics.hanzo.ai/script.js" data-website-id="ebdee691-6316-4b97-ba48-bc6b914e2d20" data-do-not-track="true" data-exclude-search="true" />
@@ -0,0 +1,93 @@
---
title: A/B Testing & Experiments
description: Run statistically rigorous experiments to make data-driven product decisions.
---
# A/B Testing & Experiments
Run controlled experiments with automatic statistical analysis. Test UI changes, new features, pricing, copy, and more.
## Overview
Experiments in Hanzo Insights use feature flags under the hood, giving you full control over rollout percentage, targeting, and stopping conditions.
## Experiment Types
### A/B Test
Split traffic between control and variant. Classic 50/50 or any custom split.
### Multi-Variant Test
Test multiple variants simultaneously (A/B/C/D). Automatically accounts for multiple comparisons.
### Holdout Test
Measure long-term impact by maintaining a permanent holdout group.
## Statistical Methods
- **Frequentist**: p-values, confidence intervals, Bayesian-adjusted thresholds
- **Bayesian**: Probability of being best, expected loss
- **Sequential testing**: Stop early when significance is reached (reduces sample waste)
Hanzo Insights uses a **Bayesian approach by default** with a configurable significance threshold (default: 95%).
## Setting Up an Experiment
### 1. Create via UI
Navigate to **Experiments** → **New Experiment**:
1. Choose your primary metric (conversion, retention, revenue)
2. Set minimum detectable effect (MDE) to calculate required sample size
3. Configure targeting conditions (same as Feature Flags)
4. Launch when ready
### 2. Create via API
```bash
curl -X POST https://app.insights.hanzo.ai/api/projects/{project_id}/experiments/ \
-H "Authorization: Bearer $PERSONAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Checkout Button Color Test",
"description": "Test green vs blue checkout button",
"feature_flag_key": "checkout-button-color",
"filters": {},
"metrics": [{"id": "purchase_completed", "type": "primary"}]
}'
```
### 3. Integrate in Code
```javascript
const variant = posthog.getFeatureFlag('checkout-button-color')
if (variant === 'control') {
// Show blue button
} else if (variant === 'test') {
// Show green button
}
// Track conversion
posthog.capture('purchase_completed', { variant })
```
## Analyzing Results
The experiment dashboard shows:
- Conversion rates per variant with confidence intervals
- Statistical significance (p-value)
- Estimated impact on revenue/retention
- Sample size progress toward significance
- Time-series graph of variant performance
## Stopping an Experiment
Stop when:
1. Statistical significance is reached (green indicator)
2. You've reached your minimum sample size
3. A winner is declared with >95% probability
**Ship the winner**: One-click to update the feature flag to 100% rollout.
## Self-Hosting Notes
Experiments require ClickHouse for statistical computations. Ensure `CLICKHOUSE_HOST` is configured. See [Self-Hosting Guide](/docs/self-hosting).
@@ -0,0 +1,129 @@
---
title: LLM Analytics
description: Monitor, debug, and optimize your AI applications with built-in LLM observability.
---
# LLM Analytics
Purpose-built observability for AI applications. Track token usage, latency, cost, quality, and user feedback across every LLM call.
## Overview
LLM Analytics extends Hanzo Insights' event pipeline with AI-specific primitives: traces, spans, generations, and evaluations. Works with any LLM provider — OpenAI, Anthropic, Google, or your own models via Hanzo's LLM Gateway.
## Core Concepts
### Traces
A trace represents a complete user interaction (e.g., one chat turn or one agent run). Traces contain one or more spans.
### Spans
A span represents a single step: an LLM call, a tool use, a retrieval, or a custom operation.
### Generations
A generation is an LLM call with full input/output capture, token counts, model name, and latency.
## Instrumentation
### Auto-Instrumentation (Hanzo LLM Gateway)
If routing through `llm.hanzo.ai`, all calls are automatically traced:
```bash
curl https://llm.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer $HANZO_API_KEY" \
-d '{"model": "zen-4o", "messages": [{"role": "user", "content": "Hello"}]}'
# → Automatically creates trace + generation in Insights
```
### Manual Instrumentation (Python)
```python
from hanzo import HanzoInsights
insights = HanzoInsights(
api_key="<YOUR_PROJECT_API_KEY>",
host="https://app.insights.hanzo.ai"
)
# Wrap LLM calls
with insights.trace(name="chat-response", user_id="user_123") as trace:
with trace.span(name="generate", input={"messages": messages}) as span:
response = openai.chat.completions.create(
model="gpt-4o",
messages=messages
)
span.end(
output=response.choices[0].message.content,
usage={"input": response.usage.prompt_tokens, "output": response.usage.completion_tokens},
model="gpt-4o",
latency=span.elapsed_ms
)
```
### OpenTelemetry Support
```python
from opentelemetry.sdk.trace import TracerProvider
from hanzo.integrations.otel import HanzoSpanExporter
provider = TracerProvider()
provider.add_span_processor(HanzoSpanExporter(api_key="<KEY>", host="https://app.insights.hanzo.ai"))
```
## Metrics Dashboard
| Metric | Description |
|--------|-------------|
| **Total Traces** | Number of user interactions |
| **Avg Latency (P50/P95/P99)** | Time-to-first-token and total latency |
| **Token Usage** | Input + output tokens by model |
| **Cost** | Estimated cost per model (configurable) |
| **Error Rate** | Failed generations by error type |
| **Cache Hit Rate** | Savings from semantic/exact caching |
## Quality & Evaluation
### Human Feedback
Capture thumbs up/down or star ratings from users:
```javascript
// After showing AI response
posthog.capture('llm_feedback', {
trace_id: '<trace_id>',
rating: 'positive', // or 'negative'
comment: 'Great answer!'
})
```
### Automated Evaluations
Run evals on sampled traces using LLM-as-judge:
```python
insights.evaluate(
trace_id=trace.id,
evaluator="helpfulness", # built-in evaluators
model="claude-sonnet-4-6", # judge model
)
```
## Prompt Management
Version and A/B test prompts without deploying:
```python
# Fetch prompt from registry
prompt = insights.get_prompt("chat-system-prompt", version="production")
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": prompt.compile(user_name=name)},
*messages
]
)
```
## Self-Hosting Notes
LLM Analytics uses ClickHouse for trace storage. The `plugin-server` processes and enriches traces async. For high-volume deployments, scale the plugin-server horizontally.
See [Self-Hosting Guide](/docs/self-hosting) for configuration details.
@@ -0,0 +1,87 @@
---
title: Session Replay
description: Record and replay user sessions to understand exactly how users interact with your product.
---
# Session Replay
Record, replay, and analyze real user sessions to understand friction points, debug issues, and improve UX.
## Overview
Session Replay captures every click, scroll, mouse movement, and network request — without recording passwords or sensitive inputs. Replays are stored on your own infrastructure.
## Features
### Automatic Capture
- Records all user interactions without code changes (1 SDK line)
- Captures DOM mutations, network requests, console logs, and errors
- Privacy-safe: auto-masks input fields, passwords, and card numbers
### Console & Network Logs
- See JavaScript errors and warnings alongside the replay
- Inspect network requests and responses during the session
- Filter by error type or status code
### Event Timeline
- Jump to specific custom events (e.g., "checkout_started")
- See feature flag states at any point in the session
- Correlate replays with funnels and user paths
### Session Filters
- Filter by country, device, browser, OS
- Filter by rage clicks, dead clicks, or error events
- Search by user properties or custom events
## Getting Started
```bash
# Already tracking events? Session Replay is automatic.
# Just enable it in Project Settings → Session Recording
```
Or enable programmatically:
```javascript
posthog.init('<YOUR_PROJECT_API_KEY>', {
api_host: 'https://app.insights.hanzo.ai',
session_recording: {
recordCrossOriginIframes: true,
}
})
```
## Privacy Controls
All sensitive fields are masked by default. You can customize masking:
```javascript
// Unmask specific elements
posthog.init('<YOUR_PROJECT_API_KEY>', {
session_recording: {
maskAllInputs: false,
maskInputOptions: {
password: true, // still mask passwords
},
}
})
```
Or use CSS classes:
```html
<!-- Always mask this element -->
<div class="ph-no-capture">Sensitive content</div>
<!-- Never mask this element -->
<input class="ph-no-mask" type="text" />
```
## Storage & Retention
Sessions are stored in your MinIO/S3 bucket. Default retention is 30 days (configurable). Full-resolution recordings are compressed using LZ-string encoding.
## Self-Hosting Notes
Session Replay requires the `plugin-server` component to process and store recordings. Ensure your `OBJECT_STORAGE_ENDPOINT` is configured in your deployment.
See [Self-Hosting Guide](/docs/self-hosting) for setup instructions.
@@ -0,0 +1,93 @@
---
title: Surveys
description: Collect in-product feedback with targeted, customizable surveys.
---
# Surveys
Collect qualitative feedback directly from your users at the right moment — without leaving your product.
## Overview
Surveys appear as in-product popups, triggered by events, user properties, or feature flag conditions. Responses are automatically correlated with behavioral data.
## Survey Types
### NPS (Net Promoter Score)
The classic 0–10 scale with optional follow-up text question. Auto-calculates your NPS score over time.
### CSAT (Customer Satisfaction)
1–5 star rating or emoji scale. Ideal for post-interaction feedback.
### Multiple Choice
Single or multi-select options. Good for feature prioritization or demographic research.
### Open Text
Free-form response. Best for discovery research and bug reports.
### Rating Scale
Custom numeric or labeled scale (e.g., "Not useful" → "Very useful").
### Link Survey
Send surveys via email or link — no in-product integration required.
## Targeting
Surveys are shown based on:
- **User properties**: plan, role, signup_date, country
- **Events**: "completed_onboarding", "viewed_pricing"
- **Feature flags**: only show to beta users
- **Frequency**: once per user, once per month, always
```javascript
// Example: trigger survey after user completes key action
posthog.capture('completed_first_report')
// → Survey configured to show after this event fires
```
## Customization
### Styling
```javascript
posthog.init('<KEY>', {
surveys: {
forceShow: false,
// Custom colors, fonts, positioning
}
})
```
### Programmatic Control
```javascript
// Show survey manually
posthog.getSurveys((surveys) => {
const mySurvey = surveys.find(s => s.name === 'Q1 NPS')
posthog.renderSurvey(mySurvey.id, '#survey-container')
})
// Check if user has seen survey
posthog.isSurveySeen(surveyId) // returns boolean
```
## Analyzing Responses
The Surveys dashboard shows:
- Response rate over time
- NPS breakdown (Promoters / Passives / Detractors)
- Individual responses with user context (events, feature flags, session replay link)
- Word cloud for open-text responses
- Export to CSV
## API Access
```bash
# List surveys
GET /api/projects/{project_id}/surveys/
# Get responses
GET /api/projects/{project_id}/surveys/{survey_id}/responses/
```
## Self-Hosting Notes
Surveys are rendered from the `@posthog/surveys-js` package embedded in the JS SDK. No additional infrastructure required beyond the standard Hanzo Insights stack.
+1 -1
View File
@@ -7,6 +7,6 @@ const create = browser<typeof Config, import("@hanzo/docs-mdx/runtime/types").In
}
}>();
const browserCollections = {
docs: create.doc("docs", {"index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "api/index.mdx": () => import("../content/docs/api/index.mdx?collection=docs"), "getting-started/index.mdx": () => import("../content/docs/getting-started/index.mdx?collection=docs"), "getting-started/quickstart.mdx": () => import("../content/docs/getting-started/quickstart.mdx?collection=docs"), "privacy/index.mdx": () => import("../content/docs/privacy/index.mdx?collection=docs"), "products/feature-flags.mdx": () => import("../content/docs/products/feature-flags.mdx?collection=docs"), "products/index.mdx": () => import("../content/docs/products/index.mdx?collection=docs"), "self-hosting/index.mdx": () => import("../content/docs/self-hosting/index.mdx?collection=docs"), }),
docs: create.doc("docs", {"index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "api/index.mdx": () => import("../content/docs/api/index.mdx?collection=docs"), "getting-started/index.mdx": () => import("../content/docs/getting-started/index.mdx?collection=docs"), "getting-started/quickstart.mdx": () => import("../content/docs/getting-started/quickstart.mdx?collection=docs"), "privacy/index.mdx": () => import("../content/docs/privacy/index.mdx?collection=docs"), "products/experiments.mdx": () => import("../content/docs/products/experiments.mdx?collection=docs"), "products/feature-flags.mdx": () => import("../content/docs/products/feature-flags.mdx?collection=docs"), "products/index.mdx": () => import("../content/docs/products/index.mdx?collection=docs"), "products/llm-analytics.mdx": () => import("../content/docs/products/llm-analytics.mdx?collection=docs"), "products/replay.mdx": () => import("../content/docs/products/replay.mdx?collection=docs"), "products/surveys.mdx": () => import("../content/docs/products/surveys.mdx?collection=docs"), "self-hosting/index.mdx": () => import("../content/docs/self-hosting/index.mdx?collection=docs"), }),
};
export default browserCollections;
+8 -4
View File
@@ -1,7 +1,11 @@
// @ts-nocheck
import * as __fd_glob_13 from "../content/docs/self-hosting/index.mdx?collection=docs"
import * as __fd_glob_12 from "../content/docs/products/index.mdx?collection=docs"
import * as __fd_glob_11 from "../content/docs/products/feature-flags.mdx?collection=docs"
import * as __fd_glob_17 from "../content/docs/self-hosting/index.mdx?collection=docs"
import * as __fd_glob_16 from "../content/docs/products/surveys.mdx?collection=docs"
import * as __fd_glob_15 from "../content/docs/products/replay.mdx?collection=docs"
import * as __fd_glob_14 from "../content/docs/products/llm-analytics.mdx?collection=docs"
import * as __fd_glob_13 from "../content/docs/products/index.mdx?collection=docs"
import * as __fd_glob_12 from "../content/docs/products/feature-flags.mdx?collection=docs"
import * as __fd_glob_11 from "../content/docs/products/experiments.mdx?collection=docs"
import * as __fd_glob_10 from "../content/docs/privacy/index.mdx?collection=docs"
import * as __fd_glob_9 from "../content/docs/getting-started/quickstart.mdx?collection=docs"
import * as __fd_glob_8 from "../content/docs/getting-started/index.mdx?collection=docs"
@@ -21,4 +25,4 @@ const create = server<typeof Config, import("@hanzo/docs-mdx/runtime/types").Int
}
}>({"doc":{"passthroughs":["extractedReferences"]}});
export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "api/meta.json": __fd_glob_1, "getting-started/meta.json": __fd_glob_2, "privacy/meta.json": __fd_glob_3, "products/meta.json": __fd_glob_4, "self-hosting/meta.json": __fd_glob_5, }, {"index.mdx": __fd_glob_6, "api/index.mdx": __fd_glob_7, "getting-started/index.mdx": __fd_glob_8, "getting-started/quickstart.mdx": __fd_glob_9, "privacy/index.mdx": __fd_glob_10, "products/feature-flags.mdx": __fd_glob_11, "products/index.mdx": __fd_glob_12, "self-hosting/index.mdx": __fd_glob_13, });
export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "api/meta.json": __fd_glob_1, "getting-started/meta.json": __fd_glob_2, "privacy/meta.json": __fd_glob_3, "products/meta.json": __fd_glob_4, "self-hosting/meta.json": __fd_glob_5, }, {"index.mdx": __fd_glob_6, "api/index.mdx": __fd_glob_7, "getting-started/index.mdx": __fd_glob_8, "getting-started/quickstart.mdx": __fd_glob_9, "privacy/index.mdx": __fd_glob_10, "products/experiments.mdx": __fd_glob_11, "products/feature-flags.mdx": __fd_glob_12, "products/index.mdx": __fd_glob_13, "products/llm-analytics.mdx": __fd_glob_14, "products/replay.mdx": __fd_glob_15, "products/surveys.mdx": __fd_glob_16, "self-hosting/index.mdx": __fd_glob_17, });