Cursor
Send Cursor IDE session telemetry — prompts, tool calls, subagent spans, and token usage — to Last9 via OpenTelemetry.
Cursor generates a lot of signal. Sessions, prompts, tool calls, model switches, compactions, subagents. None of it goes anywhere by default. 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
- Last9 account — Sign up at app.last9.io
- Cursor IDE — Any version with hook support
- Node.js 20+ — Required to run the cursorscope ingestor
- OTLP credentials — Get your endpoint and auth header from Integrations → OpenTelemetry
Setup
-
Get your Last9 OTLP credentials
Navigate to Integrations → OpenTelemetry in your Last9 dashboard. Copy:
- OTLP Endpoint (e.g.,
https://otlp-aps1.last9.io:443) - Authorization header (e.g.,
Basic <base64-token>)
- OTLP Endpoint (e.g.,
-
Run guided setup
npx @last9/cursorscopeThis opens the Last9 OpenTelemetry integration page (or prints the link), then prompts for your endpoint and auth token.
npx @last9/cursorscope setup --last9 \--otlp-base <your-last9-otlp-endpoint> \--auth-token "$LAST9_OTLP_TOKEN"Setup installs cursorscope to
~/.cursorscope, writes.envwith your credentials, registers global Cursor hooks in~/.cursor/hooks.json, and starts the ingestor. -
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. -
Verify data is arriving
- Traces — open Traces in Last9, filter by
service.name = cursorscope - Metrics — open Metrics, search for
cursor_prompt_totalorgen_ai_client_operation_duration - Logs — open Logs, filter by
service.name = cursorscope
Check ingestor health:
curl http://127.0.0.1:4327/healthz# {"ok":true,"service":"cursorscope","version":"..."} - Traces — open Traces in Last9, filter by
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 |
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 → sessionEndPer-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.jsonshould contain entries callingcursorscope-forward.sh
- Confirm the ingestor is running:
-
Export failing with
ENOTFOUNDorETIMEDOUTDNS resolution failures happen after VPN toggles or laptop sleep/wake. The ingestor’s DNS resolver doesn’t retry cached failures. Fix: restart the ingestor.
npx @last9/cursorscope stopnpx @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.logforrestarting (running vX, expected vY)entries and re-runnpx @last9/cursorscope setup. -
cursor.userappears asunknownin spanscursorscope reads user identity from the
user_emailfield in Cursor’s hook payload. This field is populated when Cursor is signed in. If you seeunknown, confirm you are logged into Cursor. -
Metrics appear but traces are missing
Traces require at least one full session event cycle (
sessionStart→sessionEndorafterAgentResponse). Verify you sent an Agent message (not just opened Cursor). Check~/.cursor/cursorscope.logfor span-related errors. -
401 / authentication errors
- Verify header format:
Authorization=Basic <token>(noBearerprefix, no extra quotes) - Regenerate the token from Integrations → OpenTelemetry if it has expired
- Verify header format:
-
Metric names in Last9
Last9 converts OTel metric names to Prometheus format. Examples:
cursorscope metric Last9 metric name cursor_prompt_totalcursor_prompt_totalcursor_tool_executions_totalcursor_tool_executions_totalgen_ai.client.operation.durationgen_ai_client_operation_duration_secondsgen_ai.client.token.usagegen_ai_client_token_usage_tokens
Please get in touch with us on Discord or Email if you have any questions.