Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
264 changes: 241 additions & 23 deletions enterprise/analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@

This guide is for users who want to explore analytics on their OpenHands Enterprise conversations.

It covers both supported install paths:

- **Replicated (VM install)** -- if you followed the [Quick Start](/enterprise/quick-start) and
manage OpenHands through the Replicated Admin Console.
- **Helm (Kubernetes install)** -- if you deployed the `openhands` Helm chart into your own
Kubernetes cluster (see [Kubernetes Installation](/enterprise/k8s-install/index)).

Most of the workflow (creating a Laminar project, creating an API key, viewing traces) is
the same on both paths. The two install-specific steps are tabbed below.

### Why Laminar?

[Laminar](https://laminar.sh/) is an open source observability platform for AI agents like OpenHands.
Expand All @@ -29,35 +39,203 @@

### Prerequisites

Before you begin, make sure you completed the [Quick Start guide](/enterprise/quick-start).
Before you begin, make sure you have completed the install for your path:

## Enable Analytics
- **Replicated**: [Quick Start guide](/enterprise/quick-start)
- **Helm**: [Kubernetes Installation guide](/enterprise/k8s-install/index)

You should see an **Analytics Configuration** section on the application configuration page.
You will also need:

Check the **Enable Analytics** box to have the installer set up and configure Laminar for analytics.
- DNS records (and a TLS certificate covering the SAN) for `analytics.app.<your-base-domain>`.
On Replicated, this is included in the [Quick Start DNS table](/enterprise/quick-start#dns-and-tls-setup).
On Helm, you choose the hostname yourself in `site-values.yaml`.

Check warning on line 51 in enterprise/analytics.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/analytics.mdx#L51

Did you really mean 'hostname'?
- An ingress controller already running in the cluster (Replicated installs ship Traefik; Helm
installs typically use Traefik as well -- see the [chart README](https://github.com/OpenHands/OpenHands-Cloud/blob/main/charts/openhands/README.md)).

Check warning on line 53 in enterprise/analytics.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/analytics.mdx#L53

Did you really mean 'Traefik'?

![Configure Analytics](./images/laminar-configure-analytics.png)
## Enable Analytics

<Tabs>
<Tab title="Replicated (VM Install)">
On the application configuration page in the Admin Console, find the
**Analytics Configuration** section.

Check the **Enable Analytics** box. The installer will set up Laminar and template the
required hostnames and Keycloak wiring for you.

![Configure Analytics](./images/laminar-configure-analytics.png)
</Tab>

<Tab title="Helm (Kubernetes Install)">
Add the `laminar` block to your `site-values.yaml` and set the top-level `env.LMNR_*`
keys so the application sends traces to Laminar.

This guide assumes a single-cluster install where OpenHands runtimes and Laminar run
in the same Kubernetes cluster. Traces are sent to the in-cluster Laminar Service,
so you only need **one** new DNS record and TLS SAN -- the user-facing Laminar UI
hostname (e.g. `analytics.app.<your-base-domain>`).

Replace `example.com` with your base domain, and replace `traefik` with the name of
your ingress controller's IngressClass.

```yaml
# site-values.yaml

env:
# The application sends traces to the in-cluster Laminar Service. No external
# hostname is required for ingestion.
LMNR_BASE_URL: "http://laminar-app-server-service"
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.

If it is a one cluster setup then I think this is fine

LMNR_FORCE_HTTP: "true"
LMNR_HTTP_PORT: "8000"
# LMNR_PROJECT_API_KEY is set in a later step, after you create an ingest-only key
# in the Laminar UI.

laminar:
enabled: true
global:
# Sets provider-specific defaults; not auto-detected. Use "gcp" or "aws".
cloudProvider: "gcp"
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.

🟡 Suggestion: cloudProvider: "gcp" is a concrete value in a copy-paste snippet, but the comment only lists "gcp" or "aws" as valid options. Users on Azure or bare-metal may set this to an unsupported value silently and get unexpected defaults from the upstream lmnr-helm chart.

Consider either expanding the comment to list all valid values (or link directly to the relevant section of the lmnr-helm config guide), or use a placeholder like <gcp|aws> to make it clear this must be substituted:

Suggested change
cloudProvider: "gcp"
# Sets provider-specific defaults; not auto-detected. Valid values: "gcp", "aws".
# See https://github.com/lmnr-ai/lmnr-helm/blob/main/CONFIGURATION.md for other environments.
cloudProvider: "gcp"

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.

Some customers will be in AWS? We may want to specify to the user with a comment specify your cloud provider AWS or GCP.

The cloud provider also impacts laminar storage. Customers might be using a different storage type than the default. Defaults are listed here in the laminar chart values https://github.com/lmnr-ai/lmnr-helm/blob/main/charts/laminar/values.yaml#L837. The GCP default pd-balanced was not available for us in the SaaS GKE cluster. For SaaS in GCP GKE, there's hyperdisk-balanced https://github.com/OpenHands/deploy/blob/main/openhands/envs/production/values.yaml#L183. I checked in the cluster and that was the storage class type available.

frontend:
ingress:
enabled: true
hostname: "analytics.app.example.com" # REQUIRED -- the Laminar UI hostname
className: "traefik" # your ingress controller's IngressClass
externalDns:
enabled: false # true if external-dns manages your DNS
tls:
enabled: true
clusterIssuer: "" # see TLS options below
secretName: "laminar-frontend-tls"
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.

The customer can reuse the openhands cert secret here instead

env:
# Must match laminar.frontend.ingress.hostname above.
nextauthUrl: "https://analytics.app.example.com"
nextPublicUrl: "https://analytics.app.example.com"
extraEnv:
# Wires the Laminar UI to your existing Keycloak realm so users can sign in
# with the same identity provider they use for OpenHands.
- name: AUTH_KEYCLOAK_ID
valueFrom:
secretKeyRef:
name: keycloak-realm
key: client-id
- name: AUTH_KEYCLOAK_SECRET
valueFrom:
secretKeyRef:
name: keycloak-realm
key: client-secret
- name: AUTH_KEYCLOAK_ISSUER
value: "https://auth.app.example.com/realms/allhands"
Comment on lines +115 to +126
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.

🟡 Suggestion: The config references a keycloak-realm secret that's created by the Replicated installer but may not exist in Helm installs. Add a note explaining where this comes from:

<Note>
  The `keycloak-realm` secret is created automatically by the OpenHands Helm chart during initial install. If you need to update Keycloak configuration, see the [chart documentation](https://github.com/OpenHands/OpenHands-Cloud/blob/main/charts/openhands/README.md).
</Note>

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.

🟡 Suggestion: The instructions above say "Replace example.com with your base domain" but don't address the allhands realm name in the issuer URL. If allhands is always the fixed Keycloak realm in OpenHands Enterprise, that should be stated explicitly so users don't wonder whether it's a placeholder. If it can vary, it should be listed as a substitution alongside example.com.

A brief inline comment would resolve the ambiguity:

Suggested change
value: "https://auth.app.example.com/realms/allhands"
- name: AUTH_KEYCLOAK_ISSUER
value: "https://auth.app.example.com/realms/allhands" # realm name is fixed as "allhands" in OpenHands Enterprise

```

### TLS options for the frontend ingress

The `laminar.frontend.ingress.tls` block above works with either pattern:

- **Pre-existing TLS secret** (recommended if your DNS and certs are managed externally):
leave `clusterIssuer: ""` and create the secret yourself. Concatenate the certificate
and CA bundle into a full chain first:

```bash
cat cert.pem ca-bundle.pem > fullchain.pem

kubectl create secret tls laminar-frontend-tls \
-n openhands --cert=fullchain.pem --key=private-key.pem
```

- **cert-manager with Let's Encrypt**: set `clusterIssuer: "letsencrypt"` (or the name
of any other `ClusterIssuer` in your cluster). The hostname must be publicly
DNS-resolvable so Let's Encrypt can complete the HTTP-01 challenge.

### Apply the change

```bash
helm upgrade --install openhands \
--namespace openhands \
oci://ghcr.io/all-hands-ai/helm-charts/openhands \
-f site-values.yaml
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.

🟡 Suggestion: The helm upgrade --install command here (and again at line 283) doesn't pin a chart version. Without --version, running this command at a later date will silently upgrade to whatever the latest OCI tag is at that time, which could introduce breaking changes in a production cluster.

Consider adding a note that users should pin the version to match what they originally installed, e.g.:

Suggested change
-f site-values.yaml
oci://ghcr.io/all-hands-ai/helm-charts/openhands \
--version <chart-version> \ # use the same version as your existing install; omit to upgrade to latest
-f site-values.yaml

Alternatively, a brief prose note like "If you want to stay on your current chart version, add --version <X.Y.Z>." would be sufficient.

```

<Accordion title="Advanced: expose the app-server externally (multi-cluster only)">
Skip this section unless OpenHands runtimes will be sending traces from **outside**
the cluster where Laminar runs. Almost all installs are single-cluster and should
use the in-cluster ingest configuration above.

For multi-cluster setups, expose the Laminar app-server through your ingress (or, on
AWS, an L4 Network Load Balancer) and point `LMNR_BASE_URL` at the external hostname.
This adds a second DNS record and TLS SAN (e.g. `laminar-api.app.<your-base-domain>`).

```yaml
# site-values.yaml

env:
LMNR_BASE_URL: "https://laminar-api.app.example.com"
LMNR_FORCE_HTTP: "true"
# Omit LMNR_HTTP_PORT -- the port comes from the URL.

laminar:
appServer:
ingress:
hostname: "laminar-api.app.example.com"
className: "traefik"
externalDns:
enabled: false
tls:
enabled: true
clusterIssuer: ""
secretName: "laminar-app-server-tls"
```

**AWS clusters** can swap `laminar.appServer.ingress` for an L4 Network Load Balancer:

```yaml
laminar:
appServer:
loadBalancer:
enabled: true
hostname: "laminar-api.app.example.com"
```

See the [lmnr-helm configuration guide](https://github.com/lmnr-ai/lmnr-helm/blob/main/CONFIGURATION.md)
for additional DNS and TLS variants, including manual DNS and pre-existing ACM certificates.
</Accordion>
</Tab>
</Tabs>

## Deploy

OpenHands will begin deploying. You can expect the deployment status to transition from
**Missing** to **Unavailable** to **Ready**. This typically takes 10-15 minutes.
<Tabs>
<Tab title="Replicated (VM Install)">
OpenHands will begin deploying. You can expect the deployment status to transition from
**Missing** to **Unavailable** to **Ready**. This typically takes 10-15 minutes.

![Deployment in progress](./images/laminar-deploy-in-progress.png)

Click **Details** next to the deployment status to monitor individual resources. Resources
shown in orange are still deploying -- wait until all resources are ready.

![Deployment in progress](./images/laminar-deploy-in-progress.png)
![Deployment status details](./images/laminar-deployment-status-details.png)
</Tab>

Click **Details** next to the deployment status to monitor individual resources. Resources
shown in orange are still deploying -- wait until all resources are ready.
<Tab title="Helm (Kubernetes Install)">
Watch the Laminar pods come up in your cluster:

![Deployment status details](./images/laminar-deployment-status-details.png)
```bash
kubectl get pods -n openhands -l app.kubernetes.io/instance=openhands -w
```

You should see pods for `laminar-frontend`, `laminar-app-server`, `laminar-clickhouse`,
`laminar-postgres`, `laminar-rabbitmq`, `laminar-redis`, and the Quickwit components.
Wait until all pods are `Running` and ready before continuing.
</Tab>
</Tabs>

## Access Laminar UI

Once the deployment status shows **Ready**, navigate to `https://analytics.app.<your-base-domain>`.
Once the deployment is **Ready**, navigate to `https://analytics.app.<your-base-domain>`
(or the `laminar.frontend.ingress.hostname` you configured for the Helm install).

Click the **Continue with Keycloak** button:

Check warning on line 236 in enterprise/analytics.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/analytics.mdx#L236

Did you really mean 'Keycloak'?

![Laminar Keycloak Auth](./images/laminar-keycloak-auth.png)

Check warning on line 238 in enterprise/analytics.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/analytics.mdx#L238

Did you really mean 'Keycloak'?

## Create a Laminar project

Expand All @@ -67,29 +245,69 @@

![Laminar Listen Traces](./images/laminar-listen-traces.png)

## Create an ingest only API Key
## Create an ingest-only API Key

Important: Always use ingest API keys when deploying.
<Warning>
Always use **ingest-only** API keys for the OpenHands integration. Ingest-only keys can
only write traces -- they cannot be used to read data, so they are safe to embed in
configuration.
</Warning>

Create a key with ther right permissions. Ingest only keys are recommended as they only have write access to write traces. They cannot be used to read data.
Create a key with ingest-only permissions:

![Configure Laminar Ingest Only Key](./images/laminar-ingest-only-key.png)

## Set Laminar Project API Key to enable automatic conversation traces
## Wire the API key into the install

<Tabs>
<Tab title="Replicated (VM Install)">
Paste the ingest-only key into the **Laminar Project API Key** field in the Admin Console
configuration:

![Configure Laminar Project API Key](./images/laminar-configure-key.png)

Click **Save config**, then deploy the change:

![Laminar Deploy Again](./images/laminar-deploy-again.png)

Wait for the deployment to complete.
</Tab>

<Tab title="Helm (Kubernetes Install)">
Set the ingest-only key in your `site-values.yaml` under the top-level `env` block:

Set the ingest only key as the Laminar Project API Key in the Admin Console configuration:
```yaml
# site-values.yaml

![Configure Laminar Project API Key](./images/laminar-configure-key.png)
env:
LMNR_BASE_URL: "http://laminar-app-server-service"
LMNR_FORCE_HTTP: "true"
LMNR_HTTP_PORT: "8000"
LMNR_PROJECT_API_KEY: "<paste-your-ingest-only-key-here>"
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.

🟡 Suggestion: The main example shows the API key in plaintext YAML, which could lead users to commit secrets to version control. Consider showing the Secret-based approach as the primary example:

env:
  LMNR_BASE_URL: "https://laminar-api.app.example.com"
  LMNR_FORCE_HTTP: "true"
  LMNR_PROJECT_API_KEY:
    valueFrom:
      secretKeyRef:
        name: laminar-api-key
        key: api-key

Then show how to create the secret:

kubectl create secret generic laminar-api-key \
  -n openhands --from-literal=api-key='<your-ingest-only-key>'

```

Click **Save config**.
<Tip>
For a production install, store the key in a Kubernetes Secret and reference it from
your values file or via `--set-string` at install time, rather than committing it to
source control.
</Tip>

## Deploy Updated Configuration
Apply the change:

Deploy the config change after setting the Laminar Project API Key in the Admin Console.
```bash
helm upgrade --install openhands \
--namespace openhands \
oci://ghcr.io/all-hands-ai/helm-charts/openhands \
-f site-values.yaml
```

![Laminar Deploy Again](./images/laminar-deploy-again.png)
Wait for the rollout to complete:

Wait for the deployment to complete.
```bash
kubectl rollout status deploy/openhands -n openhands
```
</Tab>
</Tabs>

## Start a conversation

Expand Down
4 changes: 4 additions & 0 deletions enterprise/k8s-install/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Kubernetes Installation
description: Deploy OpenHands Enterprise into your own Kubernetes cluster using Helm
icon: dharmachakra

Check warning on line 4 in enterprise/k8s-install/index.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/k8s-install/index.mdx#L4

Did you really mean 'dharmachakra'?
---

OpenHands Enterprise can be deployed into an existing Kubernetes cluster using Helm.
Expand Down Expand Up @@ -35,8 +35,8 @@
|-----------|-------------|
| **OpenHands Server** | Main application server handling UI, API, and agent orchestration |
| **Runtime API** | Manages sandbox lifecycle—provisioning, scaling, and cleanup |
| **Runtimes (Sandboxes)** | Isolated containers where agents execute code |

Check warning on line 38 in enterprise/k8s-install/index.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/k8s-install/index.mdx#L38

Did you really mean 'Runtimes'?
| **Keycloak** | Identity and access management |

Check warning on line 39 in enterprise/k8s-install/index.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/k8s-install/index.mdx#L39

Did you really mean 'Keycloak'?
| **LiteLLM Proxy** | Routes requests to your LLM provider(s) |
| **PostgreSQL** | Persistent storage for application data |
| **Redis** | Caching and session management |
Expand All @@ -58,6 +58,10 @@
Configure memory, CPU, and storage for optimal performance.
</Card>

<Card title="Analytics" icon="chart-line" href="/enterprise/analytics">
Enable Laminar for LLM observability and tracing. See the **Helm (Kubernetes Install)** tab on each step.
</Card>

## Request Access

Kubernetes-based installation is currently available to select customers on request.
Expand Down
1 change: 1 addition & 0 deletions enterprise/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@
done
```

Expected: each hostname above resolves to your VM's public IP address.

Check warning on line 185 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L185

Did you really mean 'hostname'?

Check warning on line 185 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L185

Did you really mean 'VM's'?

Test that a runtime wildcard hostname resolves:

Check warning on line 187 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L187

Did you really mean 'hostname'?

```bash
getent hosts "test.runtime.${BASE_DOMAIN}" || nslookup "test.runtime.${BASE_DOMAIN}"
Expand Down Expand Up @@ -219,7 +219,7 @@
done
```

Any HTTP response code other than `000` is acceptable for reachability checks

Check warning on line 222 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L222

Did you really mean 'reachability'?
(for example `200`, `301`, `302`, `401`, `403`, `405`).

If any check fails, stop and resolve before continuing:
Expand All @@ -230,10 +230,11 @@

| Requirement | Why It Exists |
|------------|----------------|
| `443/TCP` inbound | Primary HTTPS entrypoint for users and service hostnames |

Check warning on line 233 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L233

Did you really mean 'hostnames'?
| `30000/TCP` inbound | Replicated/KOTS Admin Console for install and configuration |
| `80/TCP` inbound | HTTP entrypoint used for ingress/redirect behavior |
| `*.runtime.<domain>` DNS + cert SAN | Runtime sandboxes are addressed by dynamic runtime-specific hostnames |

Check warning on line 236 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L236

Did you really mean 'hostnames'?
| `analytics.app.<domain>` DNS + cert SAN | Hosts the Laminar UI when [Analytics](/enterprise/analytics) is enabled. Include the SAN at install time so you do not need to re-issue the certificate later. |
| `replicated.app`, `proxy.replicated.com` | Replicated control-plane/license/install paths |
| `images.r9...`, `charts.r9...`, `updates.r9...`, `install.r9...` | Vendor distribution image/chart/update/install endpoints |
| `traefik.github.io` | Embedded cluster ingress chart repository |
Expand Down Expand Up @@ -299,7 +300,7 @@
### 5. Upload TLS certificate (if not provided with the install command)

If you did not provide certificates with the `install` command, select **"Upload your own"**,
enter your base domain under **Hostname**, upload your private key and SSL certificate, then click **Continue**.

Check warning on line 303 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L303

Did you really mean 'Hostname'?

![Upload TLS certificate](./images/upload-tls-certificate.png)

Expand All @@ -324,7 +325,7 @@

### Domain Configuration

- Select **"Derive hostnames from domain (recommended)"**

Check warning on line 328 in enterprise/quick-start.mdx

View check run for this annotation

Mintlify / Mintlify Validation (allhandsai) - vale-spellcheck

enterprise/quick-start.mdx#L328

Did you really mean 'hostnames'?
- Enter your base domain (e.g., `openhands.example.com`)

### Certificate Configuration
Expand Down
Loading