Fix dev env port resolution and improve multi-worktree support (#2025)

## Summary

- Fix dev environment port isolation so OTel collector, MongoDB, and ClickHouse resolve to the correct worktree-specific ports
- Add slot-specific session cookies so multiple worktrees on localhost don't interfere with each other's login sessions
- Share NX build cache across worktrees for faster builds
- Add worktrunk project config for automated worktree lifecycle management

## Changes

### Port resolution fix

**Root `.env`**: Replace self-referential `HDX_DEV_*` vars (e.g. `HDX_DEV_OTEL_HTTP_PORT=${HDX_DEV_OTEL_HTTP_PORT:-4318}`) with plain default values. The `${VAR:-default}` syntax caused `dotenv-expand` infinite recursion, preventing `dev-env.sh` port overrides from taking effect.

**`packages/api/.env.development`**: Remove redundant self-referential port declarations and `:-default` fallbacks from `${HDX_DEV_*}` references. Ports now come exclusively from `dev-env.sh` exports or root `.env` defaults.

**`packages/app/.env.development`**: Set `HYPERDX_API_KEY` to `super-secure-ingestion-api-key` (matching the API/collector) instead of a placeholder. Remove self-referential port declarations.

**`packages/app/src/config.ts`**: Add `process.env.OTEL_EXPORTER_OTLP_ENDPOINT` as fallback for `HDX_COLLECTOR_URL` so the browser OTel SDK picks up the correct collector endpoint.

### Session cookie isolation

**`packages/api/src/api-app.ts`**: Use `connect.sid.<slot>` as the session cookie name in dev mode so multiple worktrees on `localhost` maintain independent sessions. Guarded behind `config.IS_DEV && process.env.HDX_DEV_SLOT` — production uses the default `connect.sid`.

### Shared NX build cache

**`scripts/dev-env.sh`**: Set `NX_CACHE_DIRECTORY=~/.config/hyperdx/nx-cache` so all worktrees share a single content-hash-based build cache. Unchanged packages get cache hits regardless of worktree; changed packages rebuild correctly.

### Worktrunk project config

**`.config/wt.toml`**: New project config for [worktrunk](https://worktrunk.dev) (`wt`) worktree lifecycle hooks:
- `pre-start`: Symlink `node_modules/` from primary worktree (instant, no copy)
- `post-start`: Copy `.env.local` from primary worktree
- `post-remove`: Tear down Docker stacks (`dev-down`, `dev-int-down`, `dev-e2e-down`) for the removed worktree's slot
This commit is contained in:
Warren Lee 2026-04-01 13:53:09 -07:00 committed by GitHub
parent 958ab61d51
commit dea1b669e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 55 additions and 23 deletions

27
.config/wt.toml Normal file
View file

@ -0,0 +1,27 @@
# HyperDX worktrunk project config
# See: https://worktrunk.dev
#
# This file is committed and shared with the team. It configures lifecycle
# hooks that run when creating/switching/removing worktrees via `wt`.
# ---------------------------------------------------------------------------
# pre-start: blocking — symlink node_modules from primary worktree for
# instant dependency access. If the branch needs different deps:
# rm node_modules && yarn install
# ---------------------------------------------------------------------------
pre-start = "ln -sf {{ primary_worktree_path }}/node_modules {{ worktree_path }}/node_modules"
# ---------------------------------------------------------------------------
# post-start: background tasks
# ---------------------------------------------------------------------------
[post-start]
env = "cp {{ primary_worktree_path }}/.env.local {{ worktree_path }}/.env.local 2>/dev/null && echo 'Copied .env.local' || true"
# ---------------------------------------------------------------------------
# post-remove: clean up all Docker resources for this worktree's slot
# (dev stack, integration tests, E2E tests)
# ---------------------------------------------------------------------------
[post-remove]
dev-down = "make dev-down 2>/dev/null || true"
int-down = "make dev-int-down 2>/dev/null || true"
e2e-down = "make dev-e2e-down 2>/dev/null || true"

16
.env
View file

@ -29,14 +29,14 @@ HYPERDX_OPAMP_PORT=${HYPERDX_OPAMP_PORT:-4320}
HYPERDX_BASE_PATH= HYPERDX_BASE_PATH=
# Docker service ports (overridden by scripts/dev-env.sh for isolation) # Docker service ports (overridden by scripts/dev-env.sh for isolation)
HDX_DEV_MONGO_PORT=${HDX_DEV_MONGO_PORT:-27017} HDX_DEV_MONGO_PORT=27017
HDX_DEV_CH_HTTP_PORT=${HDX_DEV_CH_HTTP_PORT:-8123} HDX_DEV_CH_HTTP_PORT=8123
HDX_DEV_CH_NATIVE_PORT=${HDX_DEV_CH_NATIVE_PORT:-9000} HDX_DEV_CH_NATIVE_PORT=9000
HDX_DEV_OTEL_HEALTH_PORT=${HDX_DEV_OTEL_HEALTH_PORT:-13133} HDX_DEV_OTEL_HEALTH_PORT=13133
HDX_DEV_OTEL_GRPC_PORT=${HDX_DEV_OTEL_GRPC_PORT:-4317} HDX_DEV_OTEL_GRPC_PORT=4317
HDX_DEV_OTEL_HTTP_PORT=${HDX_DEV_OTEL_HTTP_PORT:-4318} HDX_DEV_OTEL_HTTP_PORT=4318
HDX_DEV_OTEL_METRICS_PORT=${HDX_DEV_OTEL_METRICS_PORT:-8888} HDX_DEV_OTEL_METRICS_PORT=8888
HDX_DEV_OTEL_JSON_HTTP_PORT=${HDX_DEV_OTEL_JSON_HTTP_PORT:-14318} HDX_DEV_OTEL_JSON_HTTP_PORT=14318
# Otel/Clickhouse config # Otel/Clickhouse config
HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=default HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=default

View file

@ -1,29 +1,23 @@
HYPERDX_API_KEY="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Ports come from scripts/dev-env.sh (worktree isolation) or root .env (defaults)
# Ports are overridden by scripts/dev-env.sh for worktree isolation
HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000}
HYPERDX_OPAMP_PORT=${HYPERDX_OPAMP_PORT:-4320}
HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080}
HYPERDX_LOG_LEVEL=debug HYPERDX_LOG_LEVEL=debug
EXPRESS_SESSION_SECRET="hyperdx is cool 👋" EXPRESS_SESSION_SECRET="hyperdx is cool 👋"
FRONTEND_URL="http://localhost:${HYPERDX_APP_PORT}" FRONTEND_URL="http://localhost:${HYPERDX_APP_PORT}"
HDX_NODE_ADVANCED_NETWORK_CAPTURE=1 HDX_NODE_ADVANCED_NETWORK_CAPTURE=1
HDX_NODE_BETA_MODE=1 HDX_NODE_BETA_MODE=1
HDX_NODE_CONSOLE_CAPTURE=1 HDX_NODE_CONSOLE_CAPTURE=1
HYPERDX_API_KEY=${HYPERDX_API_KEY}
HYPERDX_LOG_LEVEL=${HYPERDX_LOG_LEVEL}
MINER_API_URL="http://localhost:5123" MINER_API_URL="http://localhost:5123"
MONGO_URI="mongodb://localhost:${HDX_DEV_MONGO_PORT:-27017}/hyperdx" MONGO_URI="mongodb://localhost:${HDX_DEV_MONGO_PORT}/hyperdx"
NODE_ENV=development NODE_ENV=development
OTEL_SERVICE_NAME="hdx-oss-dev-api" OTEL_SERVICE_NAME="hdx-oss-dev-api"
OTEL_RESOURCE_ATTRIBUTES="service.version=dev" OTEL_RESOURCE_ATTRIBUTES="service.version=dev"
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${HDX_DEV_OTEL_HTTP_PORT:-4318}" OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${HDX_DEV_OTEL_HTTP_PORT}"
PORT=${HYPERDX_API_PORT} PORT=${HYPERDX_API_PORT}
OPAMP_PORT=${HYPERDX_OPAMP_PORT} OPAMP_PORT=${HYPERDX_OPAMP_PORT}
REDIS_URL=redis://localhost:6379 REDIS_URL=redis://localhost:6379
USAGE_STATS_ENABLED=false USAGE_STATS_ENABLED=false
NODE_OPTIONS="--max-http-header-size=131072" NODE_OPTIONS="--max-http-header-size=131072"
ENABLE_SWAGGER=true ENABLE_SWAGGER=true
DEFAULT_CONNECTIONS=[{"name":"Local ClickHouse","host":"http://localhost:${HDX_DEV_CH_HTTP_PORT:-8123}","username":"default","password":""}] DEFAULT_CONNECTIONS=[{"name":"Local ClickHouse","host":"http://localhost:${HDX_DEV_CH_HTTP_PORT}","username":"default","password":""}]
DEFAULT_SOURCES=[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"TimestampTime","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"Traces","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","_id":"682586a8b1f81924e628e808","id":"682586a8b1f81924e628e808"},"connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","sessionSourceId":"Sessions"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","metricSourceId":"Metrics"},{"from":{"databaseName":"otel_json","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"JSON Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"JSON Traces","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"JSON Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","connection":"Local ClickHouse","logSourceId":"JSON Logs","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"JSON Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum"},"connection":"Local ClickHouse","logSourceId":"JSON Logs","traceSourceId":"JSON Traces"}] DEFAULT_SOURCES=[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"TimestampTime","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"Traces","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","_id":"682586a8b1f81924e628e808","id":"682586a8b1f81924e628e808"},"connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","sessionSourceId":"Sessions"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","metricSourceId":"Metrics"},{"from":{"databaseName":"otel_json","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"JSON Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"JSON Traces","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"JSON Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","connection":"Local ClickHouse","logSourceId":"JSON Logs","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"JSON Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum"},"connection":"Local ClickHouse","logSourceId":"JSON Logs","traceSourceId":"JSON Traces"}]
INGESTION_API_KEY="super-secure-ingestion-api-key" INGESTION_API_KEY="super-secure-ingestion-api-key"
HYPERDX_API_KEY=$INGESTION_API_KEY HYPERDX_API_KEY=$INGESTION_API_KEY

View file

@ -22,6 +22,11 @@ import passport from './utils/passport';
const app: express.Application = express(); const app: express.Application = express();
const sess: session.SessionOptions & { cookie: session.CookieOptions } = { const sess: session.SessionOptions & { cookie: session.CookieOptions } = {
// Use a slot-specific cookie name in dev so multiple worktrees on localhost
// don't overwrite each other's session cookies.
...(config.IS_DEV && process.env.HDX_DEV_SLOT
? { name: `connect.sid.${process.env.HDX_DEV_SLOT}` }
: {}),
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
secret: config.EXPRESS_SESSION_SECRET, secret: config.EXPRESS_SESSION_SECRET,

View file

@ -1,10 +1,8 @@
HYPERDX_API_KEY="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" HYPERDX_API_KEY="super-secure-ingestion-api-key"
# Ports are overridden by scripts/dev-env.sh for worktree isolation # Ports come from scripts/dev-env.sh (worktree isolation) or root .env (defaults)
HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000}
HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080}
SERVER_URL="http://localhost:${HYPERDX_API_PORT}" SERVER_URL="http://localhost:${HYPERDX_API_PORT}"
NODE_ENV=development NODE_ENV=development
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${HDX_DEV_OTEL_HTTP_PORT:-4318}" OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:${HDX_DEV_OTEL_HTTP_PORT}"
OTEL_SERVICE_NAME="hdx-oss-dev-app" OTEL_SERVICE_NAME="hdx-oss-dev-app"
PORT=${HYPERDX_APP_PORT} PORT=${HYPERDX_APP_PORT}
NODE_OPTIONS="--max-http-header-size=131072" NODE_OPTIONS="--max-http-header-size=131072"

View file

@ -16,6 +16,7 @@ export const HDX_EXPORTER_ENABLED =
(process.env.HDX_EXPORTER_ENABLED ?? 'true') === 'true'; (process.env.HDX_EXPORTER_ENABLED ?? 'true') === 'true';
export const HDX_COLLECTOR_URL = export const HDX_COLLECTOR_URL =
process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT ?? process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT ??
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
'http://localhost:4318'; 'http://localhost:4318';
export const IS_DEV = NODE_ENV === 'development'; export const IS_DEV = NODE_ENV === 'development';

View file

@ -59,6 +59,12 @@ HDX_DEV_OTEL_JSON_HTTP_PORT=$((31100 + HDX_DEV_SLOT))
# --- Docker Compose project name (unique per slot) --- # --- Docker Compose project name (unique per slot) ---
HDX_DEV_PROJECT="hdx-dev-${HDX_DEV_SLOT}" HDX_DEV_PROJECT="hdx-dev-${HDX_DEV_SLOT}"
# --- Shared NX build cache across all worktrees ---
# NX cache is content-hash based so changed files get cache misses (correct
# behavior). Unchanged packages reuse cached output regardless of worktree.
NX_CACHE_DIRECTORY="${HOME}/.config/hyperdx/nx-cache"
mkdir -p "$NX_CACHE_DIRECTORY"
# Export everything # Export everything
export HDX_DEV_SLOT export HDX_DEV_SLOT
export HDX_DEV_BRANCH export HDX_DEV_BRANCH
@ -75,6 +81,7 @@ export HDX_DEV_OTEL_HTTP_PORT
export HDX_DEV_OTEL_METRICS_PORT export HDX_DEV_OTEL_METRICS_PORT
export HDX_DEV_OTEL_JSON_HTTP_PORT export HDX_DEV_OTEL_JSON_HTTP_PORT
export HDX_DEV_PROJECT export HDX_DEV_PROJECT
export NX_CACHE_DIRECTORY
# --- Clean up stale Next.js state from previous sessions --- # --- Clean up stale Next.js state from previous sessions ---
# Nuke the entire .next directory to avoid stale webpack bundles, lock files, # Nuke the entire .next directory to avoid stale webpack bundles, lock files,