Skip to content

Commit 007bf4f

Browse files
committed
feat(basepath): add runtime env support for subpath deployments
1 parent d69f875 commit 007bf4f

File tree

13 files changed

+274
-24
lines changed

13 files changed

+274
-24
lines changed

.changeset/basepath-support.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
'@hyperdx/app': minor
33
'@hyperdx/api': minor
4+
'@hyperdx/common-utils': minor
45
---
56

67
feat: Add basePath support for subpath deployments via environment variables

.env

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ HYPERDX_APP_PORT=8080
2020
HYPERDX_APP_URL=http://localhost
2121
HYPERDX_LOG_LEVEL=debug
2222
HYPERDX_OPAMP_PORT=4320
23-
HYPERDX_BASE_PATH=/hyperdx
24-
HYPERDX_API_BASE_PATH=/hyperdx/api
25-
HYPERDX_OTEL_BASE_PATH=/hyperdx/otel
23+
24+
# Subpath support (leave empty for root)
25+
HYPERDX_BASE_PATH=
26+
HYPERDX_API_BASE_PATH=
27+
HYPERDX_OTEL_BASE_PATH=
2628

2729
# Otel/Clickhouse config
2830
HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=default

packages/api/src/api-app.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getApiBasePath } from '@hyperdx/common-utils/dist/basePath';
12
import compression from 'compression';
23
import MongoStore from 'connect-mongo';
34
import express from 'express';
@@ -20,7 +21,7 @@ import passport from './utils/passport';
2021

2122
const app: express.Application = express();
2223

23-
const API_BASE_PATH = process.env.HYPERDX_API_BASE_PATH || '';
24+
const API_BASE_PATH = getApiBasePath();
2425

2526
const sess: session.SessionOptions & { cookie: session.CookieOptions } = {
2627
resave: false,
@@ -83,10 +84,10 @@ if (config.USAGE_STATS_ENABLED) {
8384
// ---------------------------------------------------------------------
8485
if (API_BASE_PATH) {
8586
const apiRouter = express.Router();
86-
87+
8788
// PUBLIC ROUTES
8889
apiRouter.use('/', routers.rootRouter);
89-
90+
9091
// PRIVATE ROUTES
9192
apiRouter.use('/alerts', isUserAuthenticated, routers.alertsRouter);
9293
apiRouter.use('/dashboards', isUserAuthenticated, routers.dashboardRouter);
@@ -96,9 +97,13 @@ if (API_BASE_PATH) {
9697
apiRouter.use('/connections', isUserAuthenticated, connectionsRouter);
9798
apiRouter.use('/sources', isUserAuthenticated, sourcesRouter);
9899
apiRouter.use('/saved-search', isUserAuthenticated, savedSearchRouter);
99-
apiRouter.use('/clickhouse-proxy', isUserAuthenticated, clickhouseProxyRouter);
100+
apiRouter.use(
101+
'/clickhouse-proxy',
102+
isUserAuthenticated,
103+
clickhouseProxyRouter,
104+
);
100105
apiRouter.use('/api/v2', externalRoutersV2);
101-
106+
102107
// Only initialize Swagger in development or if explicitly enabled
103108
if (
104109
process.env.NODE_ENV !== 'production' &&
@@ -116,7 +121,7 @@ if (API_BASE_PATH) {
116121
);
117122
});
118123
}
119-
124+
120125
app.use(API_BASE_PATH, apiRouter);
121126
} else {
122127
// PUBLIC ROUTES
@@ -132,7 +137,7 @@ if (API_BASE_PATH) {
132137
app.use('/sources', isUserAuthenticated, sourcesRouter);
133138
app.use('/saved-search', isUserAuthenticated, savedSearchRouter);
134139
app.use('/clickhouse-proxy', isUserAuthenticated, clickhouseProxyRouter);
135-
140+
136141
// TODO: Separate external API routers from internal routers
137142
// ---------------------------------------------------------------------
138143
// ----------------------- External Routers ----------------------------

packages/api/src/opamp/app.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@ import { opampController } from '@/opamp/controllers/opampController';
66
// Create Express application
77
const app = express();
88

9-
const OTEL_BASE_PATH = process.env.HYPERDX_OTEL_BASE_PATH || '';
10-
119
app.disable('x-powered-by');
1210

1311
// Special body parser setup for OpAMP
1412
app.use(
15-
`${OTEL_BASE_PATH}/v1/opamp`,
13+
'/v1/opamp',
1614
express.raw({
1715
type: 'application/x-protobuf',
1816
limit: '10mb',
1917
}),
2018
);
2119

2220
// OpAMP endpoint
23-
app.post(`${OTEL_BASE_PATH}/v1/opamp`, opampController.handleOpampMessage.bind(opampController));
21+
app.post('/v1/opamp', opampController.handleOpampMessage.bind(opampController));
2422

2523
// Health check endpoint
2624
app.get('/health', (req, res) => {

packages/app/next.config.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { configureRuntimeEnv } = require('next-runtime-env/build/configure');
22
const { version } = require('./package.json');
3+
const { getFrontendBasePath } = require('@hyperdx/common-utils/dist/basePath');
34

45
configureRuntimeEnv();
56

@@ -9,7 +10,7 @@ const withNextra = require('nextra')({
910
});
1011

1112
module.exports = {
12-
basePath: process.env.HYPERDX_BASE_PATH || '',
13+
basePath: getFrontendBasePath(),
1314
experimental: {
1415
instrumentationHook: true,
1516
// External packages to prevent bundling issues with Next.js 14
@@ -58,8 +59,8 @@ module.exports = {
5859
productionBrowserSourceMaps: false,
5960
...(process.env.NEXT_OUTPUT_STANDALONE === 'true'
6061
? {
61-
output: 'standalone',
62-
}
62+
output: 'standalone',
63+
}
6364
: {}),
6465
}),
6566
};

packages/app/pages/_document.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Head, Html, Main, NextScript } from 'next/document';
2+
import { getFrontendBasePath } from '@hyperdx/common-utils/dist/basePath';
23

34
export default function Document() {
45
return (
56
<Html lang="en">
67
<Head>
7-
<script src={`${process.env.HYPERDX_BASE_PATH || ''}/__ENV.js`} />
8+
<script src={`${getFrontendBasePath()}/__ENV.js`} />
89
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js"></script>
910
<link
1011
rel="stylesheet"

packages/app/pages/api/[...all].ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
22
import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
3+
import { getApiBasePath } from '@hyperdx/common-utils/dist/basePath';
34

45
const DEFAULT_SERVER_URL = `http://127.0.0.1:${process.env.HYPERDX_API_PORT}`;
5-
const API_BASE_PATH = process.env.HYPERDX_API_BASE_PATH || '';
6+
const API_BASE_PATH = getApiBasePath();
67

78
export const config = {
89
api: {

packages/app/src/api.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import Router from 'next/router';
33
import type { HTTPError, Options, ResponsePromise } from 'ky';
44
import ky from 'ky-universal';
5+
import { getApiBasePath } from '@hyperdx/common-utils/dist/basePath';
56
import type { Alert } from '@hyperdx/common-utils/dist/types';
67
import type { UseQueryOptions } from '@tanstack/react-query';
78
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
@@ -48,10 +49,7 @@ export function loginHook(request: Request, options: any, response: Response) {
4849
}
4950

5051
// Get basePath from runtime environment
51-
const getApiPrefix = () => {
52-
const basePath = process.env.HYPERDX_BASE_PATH || '';
53-
return `${basePath}/api`;
54-
};
52+
const getApiPrefix = () => getApiBasePath();
5553

5654
export const server = ky.create({
5755
prefixUrl: getApiPrefix(),

packages/app/src/clickhouse.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// please move app-specific functions elsewhere in the app
66
// ================================
77

8+
import { getApiBasePath } from '@hyperdx/common-utils/dist/basePath';
89
import {
910
chSql,
1011
ClickhouseClientOptions,
@@ -20,7 +21,7 @@ import { getLocalConnections } from '@/connection';
2021
import api from './api';
2122
import { DEFAULT_QUERY_TIMEOUT } from './defaults';
2223

23-
const PROXY_CLICKHOUSE_HOST = `${process.env.HYPERDX_BASE_PATH || ''}/api/clickhouse-proxy`;
24+
const PROXY_CLICKHOUSE_HOST = `${getApiBasePath()}/clickhouse-proxy`;
2425

2526
export const getClickhouseClient = (
2627
options: ClickhouseClientOptions = {},
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
getApiBasePath,
3+
getFrontendBasePath,
4+
getOtelBasePath,
5+
joinPath,
6+
} from '../basePath';
7+
8+
describe('basePath utilities', () => {
9+
describe('getFrontendBasePath', () => {
10+
it('returns empty string if no env var', () => {
11+
delete process.env.HYPERDX_BASE_PATH;
12+
expect(getFrontendBasePath()).toBe('');
13+
});
14+
15+
it('returns normalized path for valid input', () => {
16+
process.env.HYPERDX_BASE_PATH = '/hyperdx';
17+
expect(getFrontendBasePath()).toBe('/hyperdx');
18+
19+
process.env.HYPERDX_BASE_PATH = 'hyperdx/';
20+
expect(getFrontendBasePath()).toBe('/hyperdx');
21+
22+
process.env.HYPERDX_BASE_PATH = '/hyperdx/';
23+
expect(getFrontendBasePath()).toBe('/hyperdx');
24+
});
25+
26+
it('returns empty for invalid paths', () => {
27+
process.env.HYPERDX_BASE_PATH = '/../invalid';
28+
expect(getFrontendBasePath()).toBe('');
29+
30+
process.env.HYPERDX_BASE_PATH = 'http://example.com';
31+
expect(getFrontendBasePath()).toBe('');
32+
});
33+
});
34+
35+
describe('getApiBasePath', () => {
36+
it('defaults to /api if no env var', () => {
37+
delete process.env.HYPERDX_API_BASE_PATH;
38+
expect(getApiBasePath()).toBe('/api');
39+
});
40+
41+
it('uses env var if set', () => {
42+
process.env.HYPERDX_API_BASE_PATH = '/hyperdx/api';
43+
expect(getApiBasePath()).toBe('/hyperdx/api');
44+
});
45+
46+
it('normalizes input', () => {
47+
process.env.HYPERDX_API_BASE_PATH = 'hyperdx/api/';
48+
expect(getApiBasePath()).toBe('/hyperdx/api');
49+
});
50+
});
51+
52+
describe('getOtelBasePath', () => {
53+
it('returns empty if no env var', () => {
54+
delete process.env.HYPERDX_OTEL_BASE_PATH;
55+
expect(getOtelBasePath()).toBe('');
56+
});
57+
58+
it('returns normalized path', () => {
59+
process.env.HYPERDX_OTEL_BASE_PATH = '/hyperdx/otel';
60+
expect(getOtelBasePath()).toBe('/hyperdx/otel');
61+
});
62+
});
63+
64+
describe('joinPath', () => {
65+
it('joins empty base with relative', () => {
66+
expect(joinPath('', '/test')).toBe('/test');
67+
});
68+
69+
it('joins valid base and relative', () => {
70+
expect(joinPath('/hyperdx', '/api')).toBe('/hyperdx/api');
71+
expect(joinPath('/hyperdx', 'api')).toBe('/hyperdx/api');
72+
});
73+
74+
it('normalizes joined path', () => {
75+
expect(joinPath('/hyperdx/', '/api/')).toBe('/hyperdx/api');
76+
});
77+
});
78+
});

0 commit comments

Comments
 (0)