Skip to content

Contributing

Contributing to NR AI Coding Observability: Preflight

This guide covers everything you need to get productive in this repo: environment setup, project architecture, code conventions, testing, and how to verify your changes end-to-end.


What Is This Project?

Preflight provides observability for AI coding assistants. When developers use tools like Claude Code, Cursor, Windsurf, or Copilot, this project captures what’s happening — tool calls, token usage, costs, efficiency patterns — and sends it all to New Relic.

MCP Server (this repo) — Hooks into Claude Code via the Model Context Protocol. Captures every tool call, computes metrics like efficiency scores and anti-pattern detection, and exposes MCP tools that Claude Code can query directly.

The MCP server uses a common transport layer (src/shared/) for event buffering, metric aggregation, and HTTP delivery to New Relic’s APIs.


Development Setup

Prerequisites

  • Node.js v22 or later (v24 recommended; use nvm install to get the version from .nvmrc)
  • npm 11.11 or later (Node 24 ships it; on Node 22, npm install -g npm@latest). Older npm strips the libc fields from package-lock.json, so npm install rewrites 90 unrelated lines. npm warns at install time when yours is too old.
  • shellcheck (brew install shellcheck), for the pre-commit hook’s lint of tracked scripts under .claude/skills/
  • jq, which npm test runs to check the release workflow’s version filters (recent macOS and GitHub’s Ubuntu runners include it; otherwise brew install jq or apt install jq)
  • A New Relic account with a license key and account ID (for cloud-path testing)

First-time setup

Terminal window
nvm install # Install the right Node version (v24, from .nvmrc)
nvm use # Activate it
npm ci # Install dependencies from the lockfile
npm run build # Build the server and the dashboard bundle, chmod +x the CLI binaries
npm link # Register preflight on PATH (required for Claude Code hooks)
npm test # Jest suite — server, CLI, metrics
npm run test:web # Vitest suite — dashboard UI
preflight setup # Interactive wizard that inits NR keys, Claude Code hooks, dashboards

To pull the latest changes and rebuild later:

Terminal window
preflight update

Commands

Command What it does
npm run build Build the server (tsc --build), chmod the CLI binaries, then typecheck and build src/web
npm run build:clean Remove build output
npm test Run the full Jest suite (maxWorkers: 1) — everything except src/web
npm run test:web Run the Vitest suite — the src/web dashboard UI
npm run test:e2e Run the Playwright suite in e2e/ (rebuilds first via pretest:e2e)
npm run test:e2e:update Rewrite the Playwright screenshot baselines for the OS you are on
npm run test:e2e:update:linux Rewrite the -linux baselines in the Playwright image CI uses (needs Docker)
npm run test:integration Run src/multi-instance.integration.test.ts
npx tsc -p tsconfig.web.json Typecheck src/web, tests included (npm run build checks source only)
npx tsc -p tsconfig.tools.json Typecheck scripts/, e2e/ and the root *.config.ts files, which no other config reaches
npm run lint ESLint over src/, scripts/, e2e/ and the root *.config.ts files
npm run format Prettier write
npm run format:check Prettier check (no writes)
npm run deploy:dashboard Deploy the default NR dashboard
npm run deploy:dashboard:all Deploy every pre-built dashboard
npm run deploy:dashboard:update Sync every pre-built dashboard in place (preserves GUID/URL)
npm run deploy:dashboard:teardown Delete every pre-built dashboard (matches by name; missing = skipped)
npm run deploy:alerts Deploy the alert policy + conditions to NR
npm run deploy:alerts:update Sync conditions on the existing alert policy in place
npm run deploy:alerts:teardown Delete the alert policy and all its conditions
npm run backfill:sessions Backfill local session JSON files from NR event history
npm run dev Start local dashboard server (--local); assumes dist/ already built
npm run dev:all Build then start local dashboard (npm run build && npm run dev)
npm run dev:full Build backend, then start backend + Vite dev server together (open http://localhost:5173)
npm run start:local Alias for npm run dev

To run a single test file:

Terminal window
npx jest -- src/metrics/cost-tracker.test.ts
npx jest -- src/shared/harvest/harvest-scheduler.test.ts

To build directly without the chmod step:

Terminal window
npx tsc -b .

Git worktrees

A new worktree starts with no node_modules/ and no .husky/_ — both are generated rather than tracked, so neither comes across with the checkout. Install in the worktree before working in it:

Terminal window
git worktree add ../preflight-my-feature -b fix/my-feature
cd ../preflight-my-feature
npm ci # Installs deps and regenerates .husky/_ via the prepare script

Working with shared code

Do not edit src/shared/ directly — it is a vendored snapshot. If you find a bug there, please open an issue at https://github.com/newrelic-experimental/preflight/issues.


Project Structure

This is a flat single-package repo. Source lives directly under src/. There is no packages/ directory and no npm workspaces.

preflight/
src/
shared/ # Transport, events, pricing, harvest scheduler (vendored snapshot)
hooks/ # Hook collector + pre/post event pairing
metrics/ # metric analyzer classes (session, cost, anti-patterns, efficiency, …)
tools/ # MCP tool handlers
proxy/ # HTTP proxy + upstream transports
storage/ # JSON session and weekly summary persistence
security/ # Audit trail + SSRF helpers
tracing/ # OTel span lifecycle
transport/ # NR ingest manager + log ingest
platforms/ # one adapter per supported platform (see docs/ADAPTERS.md) + generic MCP fallback
digest/ # Slack digest formatter and sender
install/ # preflight install / setup CLI
alerts/ # Alert TS types (JSON files live in alerts/ at repo root)
deploy/ # `deploy-dashboards` and `deploy-alerts` subcommands
alerts/ # Alert policy + condition JSON definitions (bundled into dist/data/alerts/)
dashboards/ # Pre-built NR dashboard JSON files (bundled into dist/data/dashboards/)
scripts/ # backfill-sessions.ts, check-bundle-size.ts

For a complete annotated tree, see CLAUDE.md.

Shared transport layer (src/shared/)

The foundation layer is vendored in src/shared/. Provides:

  • Event creation — createAiRequest(), createAiResponse(), serialization to NR format
  • Transport — HTTP clients for New Relic’s Events, Metric, and Logs APIs, plus an OTLP/HTTP exporter
  • Harvest scheduler — Periodic flush of buffered events (5s) and metrics (60s) with bounded retry buffers
  • Token utilities — Extract token counts from Anthropic/Gemini API responses
  • Pricing — Calculate USD cost from token counts using model-specific pricing tables
  • Logger — createLogger('name') writes structured JSON to stderr

MCP server subsystems

  • Hooks (src/hooks/) — Claude Code invokes a hook script on every tool use. The collector writes events to a local JSONL buffer. The event processor drains the buffer, pairs pre/post events, and emits ToolCallRecord objects.

  • Metrics (src/metrics/) — metric analyzers that each receive tool call records and maintain running state. Session tracking, cost tracking + forecasting, task detection, anti-pattern detection, efficiency scoring, trend analysis, collaboration profiling, and more.

  • Tools (src/tools/) — MCP tool handlers that query the metric trackers and return results. Registered via registerTools() in src/tools/session-stats.ts.

  • Proxy (src/proxy/) — HTTP proxy layer that forwards requests to upstream MCP servers while recording latency and tool call metrics.

  • Storage (src/storage/) — Local file persistence for session summaries and weekly aggregations under ~/.newrelic-preflight/.

  • Security (src/security/) — Audit trail that classifies tool calls and flags sensitive file access or destructive commands; SSRF validation for outbound URLs.

  • Tracing (src/tracing/) — OTel span management. Emits a session root span, intermediate task spans from TaskDetector boundaries, and a leaf span per ToolCallRecord.


Key Concepts

See ARCHITECTURE.md for a full data-flow diagram and component reference. The concepts below cover the most important building blocks.

ToolCallRecord

The central data type. Every tool call captured by the hooks becomes a ToolCallRecord with fields like toolName, durationMs, success, filePath, command, exitCode, etc. This record flows through all metric trackers.

HarvestScheduler

Events and metrics are buffered in memory and flushed to New Relic on a timer. Events flush every 5 seconds, metrics every 60 seconds. Failed batches are re-queued with a bounded retry buffer. The scheduler handles graceful shutdown by awaiting a final flush.

Metric Trackers

All trackers follow the same pattern:

tracker.recordToolCall(record); // feed data in
tracker.getMetrics(); // read state out
tracker.reset(sessionId); // clear for new session

Each tracker has a corresponding test file with factory helpers.

MCP (Model Context Protocol)

The server communicates with Claude Code over stdio using JSON-RPC. It registers tools that Claude Code can discover and invoke. The @modelcontextprotocol/sdk package handles the protocol; our code registers tool handlers and implements the business logic.


Code Conventions

TypeScript

  • ESM modules with .js import extensions (required for NodeNext resolution)
  • Strict mode enabled
  • readonly on all interface fields
  • interface for API contracts, type for unions and local aliases
  • Never use as any — use as unknown as T for forced coercions
  • Never add eslint-disable comments — fix the underlying issue

File organization

  • One module per file, co-located tests: foo.ts + foo.test.ts
  • Files: kebab-case naming
  • Classes: PascalCase, functions: camelCase, constants: SCREAMING_SNAKE_CASE

Import order

  1. Node.js builtins (node:fs, node:path)
  2. External packages (@modelcontextprotocol/sdk, zod)
  3. (blank line)
  4. Shared imports (../shared/index.js)
  5. Local imports (./types.js)

Logging

Every module creates a scoped logger:

import { createLogger } from '../shared/index.js';
const logger = createLogger('my-module');

Logger writes to stderr as JSON. Never write to stdout — it’s reserved for the MCP stdio transport.

Error handling

  • Failed network sends re-queue batches for retry (bounded buffer)
  • Graceful degradation: if a tracker is unavailable, tools return sensible defaults
  • try/catch around file I/O operations with logger warnings
  • Clock skew protection: Math.max(0, ...) on computed durations

Testing

Tests live next to the code they test (foo.test.ts alongside foo.ts).

The three suites

npm test is not the whole story — there are three runners, split by what they can execute:

Suite Command Covers Runner
Server npm test src/**/*.test.ts outside src/web, plus test/**/*.test.ts Jest, node env
Web npm run test:web src/web/**/*.test.{ts,tsx} Vitest, jsdom env
Browser npm run test:e2e e2e/*.spec.ts Playwright, real Chromium

Two files sit outside all three: jest.config.ts also ignores src/shared/index.test.ts, which asserts an invariant that does not hold for vendored source, and src/multi-instance.integration.test.ts, which spawns real processes and runs on demand via npm run test:integration. Both carry a comment saying why.

npm run test:e2e installs its browser if needed, rebuilds, then starts two dashboards itself — one over an empty store, one over a seeded one — each against its own temp directory, which also stands in for HOME. So it needs no running server, and it reads none of your credentials, your real ~/.newrelic-preflight, or your Claude Code and Copilot transcripts. See TEST_PATTERNS.md for adding a test.

.claude/skills/verify-preflight/ is the only project skill tracked in git. It launches an isolated Preflight from your checkout and drives it through the collector, the dashboard, and the stdio MCP tools (.claude/skills/verify-preflight/SKILL.md). Any other skill in .claude/skills/, including symlinks to skills kept elsewhere, stays ignored. To track a new project skill, add a !.claude/skills/<name>/ line to .gitignore. Keep personal skills untracked through .git/info/exclude. Tracked skill Markdown is covered by npm run format:check, and tracked shell scripts by npm run lint:shell (shellcheck).

Screenshot assertions compare against per-platform baselines in e2e/*-snapshots/. Two are committed for each screenshot: -darwin, recorded on a Mac, and -linux, recorded in the mcr.microsoft.com/playwright image whose tag matches @playwright/test in package.json — the image CI’s e2e job runs in, so CI checks the Linux one. When a deliberate UI change makes them stale, re-record both and commit the result:

  • macOS: npm run test:e2e:update.
  • Linux: npm run test:e2e:update:linux, from any OS with Docker running. It records in the pinned image as linux/amd64, so on Apple Silicon it runs emulated and takes a few minutes. Your checkout has to be under a directory Docker shares with its VM (your home directory, by default).

A Linux desktop run compares against the committed -linux baseline and may fail on font differences alone; the container is the reference. There is no Windows baseline: a first run on Windows writes one and fails, and later runs compare against it. Keep it uncommitted, since nothing in CI checks it. Adding a new screenshot means committing its -darwin and -linux files together. CI’s e2e job fails on a missing -linux one; it runs Linux only, so nothing catches a missing or stale -darwin one but the next macOS run.

Writing tests

let stderrSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
});
afterEach(() => {
stderrSpy.mockRestore();
});
function makeRecord(overrides?: Partial<ToolCallRecord>): ToolCallRecord {
return { id: 'rec-001', toolName: 'Read', /* sensible defaults */ ...overrides };
}
  • Suppress logger output by mocking process.stderr.write
  • Use make* factory functions with optional Partial<T> overrides
  • Use jest.useFakeTimers() for anything time-dependent
  • Create temp directories for storage tests, clean up in afterEach

See TEST_PATTERNS.md for the full testing guide.

Before opening a PR

  • npm run build succeeds
  • npm test passes
  • npm run test:web passes (if you touched src/web)
  • npm run test:e2e passes (if you changed the dashboard UI; CI runs it too, against the -linux baselines — see The three suites for re-recording them)
  • npx tsc -p tsconfig.web.json passes (if you touched src/web; npm run build checks source only)
  • npx tsc -p tsconfig.tools.json passes (if you touched scripts/, e2e/ or a root *.config.ts)
  • npm run lint passes
  • npm run format:check passes (the pre-commit hook runs this too)
  • npm run lint:shell passes (if you touched a script under .claude/skills/; the pre-commit hook runs this too)
  • npm run check:test-ownership passes (if you added, moved or renamed a test file, or touched jest.config.ts, vitest.config.ts or a test:* script)
  • package-lock.json is unchanged by npm install --package-lock-only unless you changed dependencies
  • You added a changeset if the PR changes anything that ships, and left the version, CHANGELOG.md, and other PRs’ pending changesets alone (see Changesets; CI’s changeset job checks all three)
  • You’ve reviewed your own diff

Contributing Changes

External contributors — fork workflow

  1. Fork the repo on GitHub
  2. Clone your fork: git clone https://github.com/<your-username>/preflight
  3. Create a branch: git checkout -b fix/my-fix
  4. Make your changes, then run the checks in Before opening a PR
  5. Push to your fork and open a PR against main

Commit messages

Type: Short description

Types: Fix, Feat, Refactor, Chore, Test, Docs

If you used an AI coding assistant: add a Co-Authored-By trailer with the model name, e.g.:

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Branches

Use descriptive branch names: yourname/short-description or fix/issue-description.

Changesets

A PR never bumps Preflight’s version or edits CHANGELOG.md; only the release PR does (see Releasing). Instead, the PR adds a changeset: a Markdown file in .changeset/ that names the semver bump the change needs and holds its CHANGELOG entry.

---
'@newrelic/preflight': minor
---
**Scripts and fleet tooling could read `preflight doctor` results only by parsing its
human-readable output.** `preflight doctor --json` prints the checks as a JSON array on stdout,
with the same exit codes as the human-readable mode.

Create one with npx changeset, which asks for the bump and the summary, or in one line with npx changeset --patch @newrelic/preflight -m "..." (or --minor, --major). You can also write the file by hand. If you do, give it a name that says what it’s about, such as .changeset/doctor-json-output.md. Commit it along with the change it describes. A PR that makes several changes worth their own entries can add several changesets.

Changesets don’t conflict the way version bumps did. Every PR used to bump the version in five files and add its entry at the top of the CHANGELOG, so each merge put every other open PR in conflict. Each changeset is its own file, and the release PR is the only thing that bumps the version.

Which PRs need one. CI’s changeset job fails when a PR changes a file that ships and adds no changeset. The files that ship are listed under changedFilePatterns in .changeset/config.json: everything in src/ except tests, the plugin and Kiro power, the data directories, package.json, package-lock.json, server.json, and smithery.yaml, plus the build config that shapes dist/ (tsconfig.json, the two tsconfig.web*.json files, vite.config.ts, and postcss.config.js). The lockfile counts because the dashboard bundle in dist/web is built from the versions it pins. A PR that touches only docs, tests, CI, or scripts needs no changeset. If a PR changes a shipped file in a way users won’t notice, such as bumping a devDependency that only the tests or the build use, add an empty changeset with npx changeset --empty. That satisfies the check without adding a CHANGELOG entry. The dashboard’s own libraries (React, TanStack Query, and the rest of what Vite bundles) are devDependencies too, but bumping one changes what users get, so give it a patch changeset.

Release only refuses to run past a pending changeset that bumps the version (see Releasing). An empty changeset that merges after the release PR ships in that release without being mentioned, which is why it’s only for changes users won’t notice.

Picking the bump. The release takes the highest bump among its pending changesets.

  • patch: fixes, and changes users won’t notice.
  • minor: anything new a user can use, such as a config field, a CLI command or flag, an MCP tool, an event type or attribute, or a dashboard view.
  • major: anything that breaks a working setup, such as removing or renaming a config field, CLI flag, MCP tool, or event attribute that dashboards and alerts query, or raising the minimum Node.js version.

Writing the entry. Write it the way the existing CHANGELOG entries read: for someone who uses Preflight, about what changed for them and why, not how the code changed. A fix says what was wrong and what happens now. When an entry runs long, lead with one bolded sentence. The summary goes into the CHANGELOG exactly as written, and Prettier checks .changeset/ files like any others, so run npm run format before committing.

Changing a changeset that’s already on main. Edit its text in place, raise its bump, or rename the file without changing it. CI fails a PR that deletes a pending changeset, empties it, or lowers its bump, since any of those quietly drops a change from the next release. A maintainer can merge over the check for a PR that reverts the change a changeset describes, or when a reviewer agrees a bump should be lower. CI also fails a PR whose own changeset it can’t read, so write each bump as '@newrelic/preflight': patch (or minor, or major). If an unreadable one reaches main anyway, every PR fails and Release refuses to run until a PR fixes or removes it.

Releasing

  1. While main has changesets that need a release, release-pr.yml keeps a Chore: Release x.y.z PR open. It runs npm run version-packages, which bumps package.json, adds the CHANGELOG entry, and deletes the changesets it used. Then scripts/release-files.ts copies the new version into package-lock.json, server.json, and both plugin manifests, and dates the entry. Each push to main rebuilds the PR, so to change an entry, edit its changeset on main instead of pushing to the PR’s branch. The entry’s date is the day the PR was last rebuilt. If that’s stale, start the Release PR workflow on main from the Actions tab (Run workflow) before merging. Don’t use Re-run on an older run: a re-run builds from that run’s commit, so the PR would lose any changeset that landed since.
  2. Review the release PR, then close and reopen it right before you merge, and wait for CI. GitHub doesn’t let a PR opened by the built-in GITHUB_TOKEN start workflows, so CI doesn’t run on it by itself. A reopen runs CI on the PR merged into the current main, so it also tests whatever landed after the PR was built, such as a new test that reads the version files. Then merge it. Every push to main gives the PR a new head commit, which drops its checks and its approval. When that happens, reopen it again and approve only once CI passes: main requires the approval but not CI, so the approval is the only gate. Before merging, also check the Actions tab: no Release PR run should be queued or in progress, and the latest one shouldn’t have failed, since until a run pushes, the PR still shows the old commit’s green checks. If a push lands while the PR is closed, the workflow opens a new release PR instead; leave the old one closed, and close and reopen the new one before merging it.
  3. Right after the release PR merges, run the Release workflow (release.yml) on main from the Actions tab. Run from any other branch, it skips its job and publishes nothing. It checks that every version file agrees (npm run check:release-files), tags vX.Y.Z, creates the GitHub release, and publishes to npm, the Homebrew tap, and the MCP Registry.

Release tags and publishes main as it stands when it runs. A changeset that merges after the release PR goes into the next release’s CHANGELOG entry, so if Release ran after it, x.y.z would ship that change without mentioning it. Release refuses to run while main has such a changeset. When that happens, merge the next release PR, which release-pr.yml opens as soon as the changeset lands, and run Release then. That release also ships everything in x.y.z, which keeps its CHANGELOG entry but never reaches npm under its own number.

If Release fails after tagging, for example at the npm publish, use Re-run failed jobs on that run, which uses the commit it started from. If only the Homebrew tap step failed, the run still finishes green (that step is allowed to fail, and shows a warning), so use Re-run all jobs instead; the steps already done skip themselves. Don’t start a new run: by then main may have moved past the tagged commit, and Release refuses to run when the existing tag points at a different commit than the one it checked out, so it can’t publish that commit under the tag’s version. The GitHub release’s notes are the version’s CHANGELOG entry.

If the Release PR workflow can’t run, run npm run version-packages on a branch named changeset-release/main off main, commit the result, and open the PR yourself. CI’s changeset job skips only that branch, since a release PR bumps the version.


Security

This codebase sends telemetry to New Relic and can spawn child processes and proxy network requests:

  • Redact before you log or send. Any string that might contain secrets must pass through redact() (agent) or redactSensitive() (MCP server) before it reaches a logger or NR event.
  • Validate external strings at the boundary. accountId is validated as 1–12 decimal digits at config load. Tool names are truncated and stripped of control characters.
  • Subprocess commands need absolute paths. StdioUpstream rejects relative command names and strips dangerous env vars (LD_PRELOAD, etc.) before spawning.
  • HTTP upstream URLs are SSRF-checked. HttpUpstream rejects non-http:/https: schemes and RFC-1918/loopback hosts.
  • High security mode is absolute. When highSecurity=true, recordContent is always false. Never bypass this.

See SECURITY.md for the full guidelines and code review checklist.


Platform Support

The MCP server registers an adapter per AI coding platform and auto-detects the active one at startup — each adapter’s isSupported() checks its own platform-specific env vars (most do not use a shared NEW_RELIC_AI_PLATFORM variable; only the Kiro and Copilot adapters read it).

Platform capabilities vary: some platforms expose a real hook mechanism that captures every built-in tool call, others only support MCP as a client (so Preflight can only see calls routed to its own MCP tools, not the platform’s built-in file/shell tools).

See docs/ADAPTERS.md for the full per-platform reference — integration mechanism, detection env vars, tool-name mapping, known gaps, and setup steps.


Deploying to New Relic

Dashboards

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-dashboards --all

Deploys all seven pre-built dashboards. Use --print to output JSON for manual import via the NR UI.

For a self-reflection dashboard pre-filtered to your identity:

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-dashboards ai-coding-assistant-personal.json --developer <your-name>

To replace existing dashboards in place (preserves GUID and URL), add --update:

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-dashboards --all --update

To remove all deployed dashboards:

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-dashboards --all --teardown

Alert conditions

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-alerts

Deploys the “AI Coding Assistant Alerts” policy with five NRQL conditions. Use --dry-run to preview without hitting the API.

To sync conditions in place on an existing policy (preserves policy ID; matches conditions by name to update, creates new ones, deletes removed ones):

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-alerts --update

For per-developer alerts scoped to one identity:

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-alerts --developer <your-name>

This creates a separate policy AI Coding — Personal — <name> from alerts/conditions-personal/, with developer = '<name>' injected into every NRQL query. Use --teardown --developer <name> to remove just the personal policy.

To remove all deployed alert conditions:

Terminal window
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 preflight deploy-alerts --teardown

Terraform (IaC alternative)

A Terraform module in terraform/ deploys all 7 dashboards and all 10 alert conditions as an alternative to the scripts above. See ADVANCED.md — Terraform Deployment for full usage, variables.


Verifying Your Setup

After making changes, run through these checkpoints to confirm end-to-end behavior. Work through one path (cloud or local) at a time.

Prerequisites

Item Check
Node.js v22+ node --version → v22.x.x or later
npm v10+ npm --version → 10.x.x
Claude Code (latest) Opens and launches
Cloud path only: New Relic account with a license key + user API key See README
Terminal window
git clone https://github.com/newrelic-experimental/preflight
cd preflight
nvm use
npm ci
npm run build
npm link

Checkpoint: preflight --help prints the command list. If you see command not found, the npm link step didn’t work.

2. Run the setup wizard

Terminal window
preflight setup

For cloud path, choose cloud and supply your license key, account ID, and (optionally) your user API key. For local path, choose local.

Checkpoint: cat ~/.newrelic-preflight/config.json shows the values you entered.

3. Restart Claude Code

Quit and reopen Claude Code. The MCP server starts automatically.

Checkpoint — MCP connection: In a new Claude Code session, ask:

Call nr_observe_health and show me the result.

Expected:

{
"status": "ok",
"version": "1.0.x",
"developer": "your-name",
"session_id": "some-uuid",
"uptime_seconds": 3
}

If you see tool not found or MCP server unavailable, the server didn’t start. Check Claude Code’s MCP output panel (View → Output → MCP) for errors, then re-run preflight install and restart.

WSL users: Pass --windows-cc or --linux-cc to target the right Claude Code installation. Run preflight setup if unsure — the wizard will ask.

Checkpoint — local dashboard (local path only):

Terminal window
curl -s http://127.0.0.1:7777/api/health
# Expected: {"ok":true,"uptime":<number>}

4. Generate activity and verify

In Claude Code:

Read the file README.md and summarize it in one sentence.

Then:

Call nr_observe_get_session_stats and show me the result.

Expected: tool_calls > 0, non-zero session_duration_ms.

Cloud path: Open your NR account → Dashboards → search “AI Coding”. Within 1–2 minutes:

SELECT count(*) FROM AiToolCall WHERE developer = 'your-name' SINCE 5 minutes ago

Expected: a non-zero count.

Local path: Open http://127.0.0.1:7777 and confirm the Today tab shows tool call count > 0.

5. Smoke test anti-pattern detection

Read README.md. Read it again. Read it a third time. Now call nr_observe_get_anti_patterns.

Expected: a re_reading entry for README.md with read_count: 3.

Deploy dashboards and alerts (cloud path)

Terminal window
# Dashboards
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-dashboards --all
# Alerts (optional)
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-alerts

Re-deploying? Use --update to sync in place and avoid creating duplicates.

Checkpoint: Open NR → Dashboards → search AI Coding. You should see 7 dashboards listed.

Teardown / reset

To remove all hooks and start fresh:

Terminal window
preflight uninstall --yes # --yes skips the confirmation prompt
rm -rf ~/.newrelic-preflight
# Cloud: remove dashboards and alerts from NR
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-dashboards --all --teardown
NEW_RELIC_API_KEY=NRAK-... NEW_RELIC_ACCOUNT_ID=12345 \
preflight deploy-alerts --teardown

Then restart Claude Code.


Troubleshooting

Symptom Likely cause Fix
Anything unexpected Config, hooks, connectivity, or daemon issue Run preflight doctor — prints six checks with actionable fixes
preflight: command not found npm link not run Run npm link in the repo root
nr_observe_health returns tool-not-found MCP server not started Restart Claude Code; check MCP output panel
No data in NR after 5 minutes Wrong license key or account ID Re-run preflight setup with correct credentials
Dashboard at 7777 unreachable Port in use or mode is not local Check lsof -i:7777; confirm config.json has "mode": "local"
Hook not firing preflight not on PATH when Claude Code launched Run npm link, then restart Claude Code
Invalid account ID in wizard Entered a non-numeric value Account IDs are digits only (e.g. 3456789)

Where to Get Help

  • CLAUDE.md — Full technical reference: architecture, conventions, every pattern in detail.
  • SECURITY.md — Security practices, invariants, and code review checklist. Read before any PR touching config loading, network requests, subprocess execution, or telemetry fields.
  • TEST_PATTERNS.md — Testing conventions, factory patterns, mock strategies.
  • COMMANDS_TABLE.md — All MCP tools with parameters and return schemas.
  • METRICS_TABLE.md — Every NR event and metric, field definitions, delivery mechanism.
  • The code itself — Best examples of our patterns: src/metrics/ (tracker pattern), src/shared/harvest/ (scheduler/buffer pattern).