> ## Documentation Index
> Fetch the complete documentation index at: https://allhandsai-jpshackelford-analytics-helm-tab.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Analytics

> Get started with LLM observability and tracing in OpenHands Enterprise.

This guide walks you through setting up and using Laminar for Analytics in OpenHands Enterprise.
You'll opt into Analytics and configure conversations to automatically send traces to Laminar.

## Who This Is For

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.

Use Laminar to view your conversation traces including prompts, tool calls, and answers. A trace is a record of what your agent did.

Laminar can help you see where the agent went wrong. From traces, you can create signals. A signal is a natural language instruction to extract structured data from traces. Use signals to analyze recurring behavior across traces. You can then create better situations for prompting and measure them in Laminar. You can also analyze and improve your skills.

For example, you can view all conversation traces related to a specific skill.

Laminar can help you answer the following questions:

* On a trace, did the agent do a good job using the skill?
* On another trace, did the agent do a bad job?

For more information on evaluating skills, see [Evaluating Agent Skills](https://www.openhands.dev/blog/evaluating-agent-skills).

### Prerequisites

Before you begin, make sure you have completed the install for your path:

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

You will also need:

* 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`.
* 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)).

## 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.

    <img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-configure-analytics.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=b1e0f9768e3975ddd23ae29667acf07b" alt="Configure Analytics" width="730" height="276" data-path="enterprise/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 theme={null}
    # 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"
      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"
      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"
        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"
    ```

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

    <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 theme={null}
      # 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 theme={null}
      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

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

    <img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-deploy-in-progress.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=295440adad91b4058319ff45604ad46b" alt="Deployment in progress" width="1528" height="784" data-path="enterprise/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.

    <img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-deployment-status-details.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=e40cd3be20d313b365c99f33f9ae133f" alt="Deployment status details" width="774" height="1000" data-path="enterprise/images/laminar-deployment-status-details.png" />
  </Tab>

  <Tab title="Helm (Kubernetes Install)">
    Watch the Laminar pods come up in your cluster:

    ```bash theme={null}
    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 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:

<img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-keycloak-auth.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=523b6e7190b7ceb225cc95a4fafb416c" alt="Laminar Keycloak Auth" width="3004" height="1530" data-path="enterprise/images/laminar-keycloak-auth.png" />

## Create a Laminar project

<img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-create-project.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=24dfa5919c66863a17042b2b7be1baa9" alt="Laminar Create Project" width="2996" height="1520" data-path="enterprise/images/laminar-create-project.png" />

Once a project has been created, Laminar is ready to listen for traces.

<img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-listen-traces.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=ff3e5511e68b9d4a1d3e483c845fd5bc" alt="Laminar Listen Traces" width="3000" height="1518" data-path="enterprise/images/laminar-listen-traces.png" />

## Create an ingest-only API Key

<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 ingest-only permissions:

<img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-ingest-only-key.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=26d365ec57baa744582b263fc3c7fe37" alt="Configure Laminar Ingest Only Key" width="3004" height="1496" data-path="enterprise/images/laminar-ingest-only-key.png" />

## 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:

    <img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-configure-key.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=13ac2f13c04951467ddff3a9811aa202" alt="Configure Laminar Project API Key" width="2538" height="1564" data-path="enterprise/images/laminar-configure-key.png" />

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

    <img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-deploy-again.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=74bcf802b6a52ac6c2ec66ff1902816a" alt="Laminar Deploy Again" width="1610" height="242" data-path="enterprise/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:

    ```yaml theme={null}
    # site-values.yaml

    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>"
    ```

    <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>

    Apply the change:

    ```bash theme={null}
    helm upgrade --install openhands \
      --namespace openhands \
      oci://ghcr.io/all-hands-ai/helm-charts/openhands \
      -f site-values.yaml
    ```

    Wait for the rollout to complete:

    ```bash theme={null}
    kubectl rollout status deploy/openhands -n openhands
    ```
  </Tab>
</Tabs>

## Start a conversation

Navigate to the OpenHands UI at `https://app.<your-base-domain>`. Start a new conversation and try a prompt.

<img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-openhands-conversation.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=1fbf63ee081abd2745ad964d266eb10b" alt="Start a Conversation" width="1428" height="1520" data-path="enterprise/images/laminar-openhands-conversation.png" />

Your conversations will now automatically send a trace to Laminar.

<img src="https://mintcdn.com/allhandsai-jpshackelford-analytics-helm-tab/24ORkbZnn_XrLJRh/enterprise/images/laminar-trace.png?fit=max&auto=format&n=24ORkbZnn_XrLJRh&q=85&s=18aa0d238a68e5cb297b1c80427e7621" alt="Laminar Trace" width="3010" height="1516" data-path="enterprise/images/laminar-trace.png" />

<video controls className="w-full aspect-video" src="https://github.com/user-attachments/assets/0cdf1625-3246-4388-a989-765f00d33ffb" />

## Next Steps

<CardGroup cols={2}>
  <Card title="Prompting Best Practices" icon="lightbulb" href="/openhands/usage/tips/prompting-best-practices">
    Get the most out of your AI coding agents with effective prompting techniques.
  </Card>

  <Card title="Contact Support" icon="headset" href="https://openhands.dev/contact">
    Reach out to the OpenHands team for deployment assistance or questions.
  </Card>

  <Card title="OpenHands Documentation" icon="book" href="/overview/introduction">
    Explore the full OpenHands documentation for usage guides and features.
  </Card>
</CardGroup>
