# Cursor

> Send Cursor IDE session telemetry — prompts, tool calls, subagent spans, and token usage — to Last9 via OpenTelemetry.

Source: https://last9.io/docs/integrations/cursor/

Cursor generates a lot of signal. Sessions, prompts, tool calls, model switches, compactions, subagents. None of it goes anywhere by default. [cursorscope](https://github.com/last9/cursorscope) — an open-source collector built by Last9 — fixes that.

cursorscope is a small Node.js service that intercepts Cursor's hook events and exports them as OpenTelemetry traces, metrics, and logs to Last9. You get full observability into how your team uses Cursor: which models they reach for, how long agent loops run, where tools fail, how token budgets are spent.

It exports three OpenTelemetry signal types:

- **Traces** — one root span per session, prompt spans as children, tool call spans underneath those, subagent spans branching off when Cursor fans out work
- **Metrics** — counters and histograms for sessions, prompts, tool executions, lines of code, MCP invocations, and token usage
- **Logs** — one structured log record per hook event with conversation ID, model, and hook-specific fields

## What gets exported

### Traces

cursorscope models each Cursor session as a distributed trace. Spans are correlated by `cursor.conversation.id` and `cursor.generation.id` so tool calls nest under their parent prompt.

| Span                      | Emitted for                | Key attributes                                                                                      |
| ------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
| `invoke_agent Cursor`     | Session start, each prompt | `cursor.user`, `cursor.user.email`, `cursor.repo`, `gen_ai.request.model`, `gen_ai.conversation.id` |
| `execute_tool <name>`     | Each tool invocation       | `gen_ai.tool.name`, `gen_ai.tool.type`, `gen_ai.tool.call.id`, `cursor.attribution.category`        |
| `invoke_agent <subagent>` | Subagent start/stop        | `cursor.is_background_agent`, `cursor.subagent.model`                                               |

Span events on session spans:

| Event                                       | Emitted when                                   |
| ------------------------------------------- | ---------------------------------------------- |
| `gen_ai.client.session.end`                 | Session ends (includes duration, final status) |
| `gen_ai.client.inference.operation.details` | After agent response (includes token counts)   |

### Metrics

| Metric                                   | Unit                | Key attributes                                           |
| ---------------------------------------- | ------------------- | -------------------------------------------------------- |
| `cursor_hook_events_total`               | count               | `cursor_user`, `cursor_model`, `cursor_repo`             |
| `cursor_session_total`                   | count               | `cursor_user`, `cursor_model`, `cursor_repo`             |
| `cursor_prompt_total`                    | count               | `cursor_user`, `cursor_model`, `cursor_repo`             |
| `cursor_tool_executions_total`           | count               | `tool_name`, `success`, `cursor_user`                    |
| `cursor_lines_of_code_total`             | count               | `type` (added/removed), `cursor_user`                    |
| `cursor_mcp_invocations_total`           | count               | `cursor.mcp.server`, `cursor.mcp.tool`                   |
| `cursor_attribution_invocations_total`   | count               | `cursor.attribution.category`, `cursor.attribution.name` |
| `cursor_attributed_context_tokens_total` | count               | `cursor.attribution.category`, `cursor.attribution.name` |
| `gen_ai.client.operation.duration`       | seconds (histogram) | `gen_ai.operation.name`, `gen_ai.request.model`          |
| `gen_ai.client.token.usage`              | tokens (histogram)  | `gen_ai.token.type`, `gen_ai.request.model`              |
| `cursor_api_metric_value`                | gauge               | `metric_name` (Admin API polling)                        |

### Logs

One structured log record per hook event. All records share `cursor.conversation.id` so you can reconstruct a full session sequence in Last9 Logs.

Key log fields: `hook_event_name`, `cursor.user`, `cursor.user.email`, `cursor.repo`, `gen_ai.request.model`, `cursor.conversation.id`, `cursor.generation.id`.

## Prerequisites

1. **Last9 account** — Sign up at [app.last9.io](https://app.last9.io)
2. **Cursor IDE** — Any version with hook support
3. **Node.js 20+** — Required to run the cursorscope ingestor
4. **OTLP credentials** — Get your endpoint and auth header from [**Integrations → OpenTelemetry**](https://app.last9.io/integrations?integration=OpenTelemetry)

:::note
The OTLP auth token is only visible to **Last9 organization admins**. If you are not an admin, ask yours to copy it from the integration page.
:::

## Setup

1. **Get your Last9 OTLP credentials**

   Navigate to [**Integrations → OpenTelemetry**](https://app.last9.io/integrations?integration=OpenTelemetry) in your Last9 dashboard. Copy:

   - **OTLP Endpoint** (e.g., `https://otlp-aps1.last9.io:443`)
   - **Authorization header** (e.g., `Basic <base64-token>`)

2. **Run guided setup**

**Interactive**

   ```bash
   npx @last9/cursorscope
   ```

   This opens the Last9 OpenTelemetry integration page (or prints the link), then prompts for your endpoint and auth token.

**Non-interactive**

   ```bash
   npx @last9/cursorscope setup --last9 \
     --otlp-base <your-last9-otlp-endpoint> \
     --auth-token "$LAST9_OTLP_TOKEN"
   ```

   Setup installs cursorscope to `~/.cursorscope`, writes `.env` with your credentials, registers global Cursor hooks in `~/.cursor/hooks.json`, and starts the ingestor.

3. **Restart Cursor and send one Agent message**

   The ingestor starts in the background automatically on the next Cursor hook event. Logs at `~/.cursor/cursorscope.log`.

4. **Verify data is arriving**

   - **Traces** — open **Traces** in Last9, filter by `service.name = cursorscope`
   - **Metrics** — open **Metrics**, search for `cursor_prompt_total` or `gen_ai_client_operation_duration`
   - **Logs** — open **Logs**, filter by `service.name = cursorscope`

   Check ingestor health:

   ```bash
   curl http://127.0.0.1:4327/healthz
   # {"ok":true,"service":"cursorscope","version":"..."}
   ```

## Configuration reference

cursorscope is configured via `~/.cursorscope/.env`.

### OTLP export

| Variable                                            | Default       | Description                               |
| --------------------------------------------------- | ------------- | ----------------------------------------- |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`                | —             | Last9 traces endpoint (`/v1/traces`)      |
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`               | —             | Last9 metrics endpoint (`/v1/metrics`)    |
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`                  | —             | Last9 logs endpoint (`/v1/logs`)          |
| `OTEL_EXPORTER_OTLP_HEADERS`                        | —             | `Authorization=Basic <token>`             |
| `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | `cumulative`  | Required by Last9 — leave as `cumulative` |
| `OTEL_SERVICE_NAME`                                 | `cursorscope` | Service name tag on all signals           |

### Privacy

| Variable                  | Default | Description                                                    |
| ------------------------- | ------- | -------------------------------------------------------------- |
| `CURSOR_LOG_USER_PROMPTS` | `false` | Include full prompt text in logs                               |
| `CURSOR_LOG_TOOL_DETAILS` | `false` | Include tool input/output in spans                             |
| `CURSOR_MASK_USER_EMAIL`  | `false` | Mask `cursor.user.email` to `u*****r@domain.com` before export |

### Ingestor

| Variable                    | Default | Description                           |
| --------------------------- | ------- | ------------------------------------- |
| `PORT`                      | `4327`  | Local HTTP port for the ingestor      |
| `CURSORSCOPE_AUTO_START`    | `true`  | Auto-start ingestor when a hook fires |
| `METRIC_EXPORT_INTERVAL_MS` | `15000` | Metric flush interval in milliseconds |

### Cursor Admin API polling

For Cursor Business accounts — exports team-level daily usage as OTel gauges.

| Variable                      | Default  | Description                   |
| ----------------------------- | -------- | ----------------------------- |
| `ENABLE_CURSOR_API_POLLING`   | `false`  | Enable Admin API polling      |
| `CURSOR_ADMIN_API_KEY`        | —        | Cursor Admin API key          |
| `CURSOR_TEAM_ID`              | —        | Cursor team ID                |
| `CURSOR_API_POLL_INTERVAL_MS` | `300000` | Poll interval in milliseconds |

:::caution
`CURSOR_LOG_USER_PROMPTS=true` and `CURSOR_LOG_TOOL_DETAILS=true` export raw prompt content and tool arguments. Enable only in environments where this data is safe to export.
:::

## What you can do in Last9

### Session traces with nested tool calls

Every agent interaction produces a distributed trace rooted at `invoke_agent Cursor`. Tool calls appear as child spans with duration and success/failure status. Subagent spans branch off when Cursor fans out work in parallel.

Filter Last9 Traces by `service.name = cursorscope` and drill into any span to see the full conversation context: model, repo, user, conversation ID.

### Token usage and model efficiency (metrics)

`gen_ai.client.token.usage` is a histogram broken down by token type and model. Compare token counts across models to track efficiency as your team experiments with model selection.

### Tool call latency and failure rates (metrics + traces)

`cursor_tool_executions_total` segmented by `tool_name` and `success` shows which tools fail most often. Cross-reference with `gen_ai.client.operation.duration` to find which tools dominate session time.

### MCP server attribution (metrics)

`cursor_mcp_invocations_total` and `cursor_attributed_context_tokens_total` reveal which MCP servers are responsible for context token consumption — useful for spotting noisy servers or inefficient tool patterns.

### Lines of code output (metrics)

`cursor_lines_of_code_total` tracks lines added and removed by Cursor agent and Tab edits, broken down by user. Useful for adoption reporting and measuring AI-assisted output.

### Session replay via `cursor.conversation.id` (logs)

Every log record carries `cursor.conversation.id`. Filter Last9 Logs by a conversation ID to reconstruct the full event sequence for one session:

```
sessionStart → beforeSubmitPrompt → preToolUse → postToolUse → afterAgentResponse → sessionEnd
```

### Per-user usage breakdowns

All metrics carry `cursor_user` (the Cursor user email). Group by `cursor_user` to see per-developer prompt counts, tool usage, and token spend — useful for team leads tracking AI adoption or budget attribution.

---

## Troubleshooting

- **No data in Last9 after sending an Agent message**

  - Confirm the ingestor is running: `curl http://127.0.0.1:4327/healthz`
  - Check logs for export errors: `tail -50 ~/.cursor/cursorscope.log` — look for `[otel-export] export failed`
  - Probe OTLP connectivity: `curl http://127.0.0.1:4327/debug/otlp-probe`
  - Verify hooks are registered: `~/.cursor/hooks.json` should contain entries calling `cursorscope-forward.sh`

- **Export failing with `ENOTFOUND` or `ETIMEDOUT`**

  DNS resolution failures happen after VPN toggles or laptop sleep/wake. The ingestor's DNS resolver doesn't retry cached failures. Fix: restart the ingestor.

  ```bash
  npx @last9/cursorscope stop
  npx @last9/cursorscope start
  ```

- **Ingestor restarts on every hook event**

  Multiple concurrent hook calls can trigger version-check races in `ensure-cursorscope.sh`. This is normal during a version upgrade. If it persists, check `~/.cursor/cursorscope.log` for `restarting (running vX, expected vY)` entries and re-run `npx @last9/cursorscope setup`.

- **`cursor.user` appears as `unknown` in spans**

  cursorscope reads user identity from the `user_email` field in Cursor's hook payload. This field is populated when Cursor is signed in. If you see `unknown`, confirm you are logged into Cursor.

- **Metrics appear but traces are missing**

  Traces require at least one full session event cycle (`sessionStart` → `sessionEnd` or `afterAgentResponse`). Verify you sent an Agent message (not just opened Cursor). Check `~/.cursor/cursorscope.log` for span-related errors.

- **401 / authentication errors**

  - Verify header format: `Authorization=Basic <token>` (no `Bearer` prefix, no extra quotes)
  - Regenerate the token from [**Integrations → OpenTelemetry**](https://app.last9.io/integrations?integration=OpenTelemetry) if it has expired

- **Metric names in Last9**

  Last9 converts OTel metric names to Prometheus format. Examples:

  | cursorscope metric                 | Last9 metric name                          |
  | ---------------------------------- | ------------------------------------------ |
  | `cursor_prompt_total`              | `cursor_prompt_total`                      |
  | `cursor_tool_executions_total`     | `cursor_tool_executions_total`             |
  | `gen_ai.client.operation.duration` | `gen_ai_client_operation_duration_seconds` |
  | `gen_ai.client.token.usage`        | `gen_ai_client_token_usage_tokens`         |

Please get in touch with us on [Discord](https://discord.com/invite/Q3p2EEucx9) or [Email](mailto:support@last9.io) if you have any questions.
