feat: add build option for a ClickHouse bundled build (#1717)

References HDX-3265
Closes HDX-3389

Adds a build that we can use in ClickHouse. 

This build enables bundling HyperDX with ClickHouse https://github.com/ClickHouse/ClickHouse/pull/96597
This commit is contained in:
Aaron Knudtson 2026-02-12 13:05:32 -05:00 committed by GitHub
parent ebbfa2410e
commit ce09b59b1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 264 additions and 20 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
feat: add static build generation

View file

@ -306,3 +306,46 @@ jobs:
echo "::error::One or more E2E test shards failed"
exit 1
fi
clickhouse-static-build:
name: ClickHouse Bundle Build
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
strategy:
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache-dependency-path: 'yarn.lock'
cache: 'yarn'
- name: Install dependencies
run: yarn install
- name: Build App
run: yarn build:clickhouse
- name: Verify output directory exists and has size
run: |
if [ ! -d "packages/app/out" ]; then
echo "::error::Output directory 'packages/app/out' does not exist"
exit 1
fi
echo "✓ Output directory exists"
# Calculate size in bytes and convert to MB
size_kb=$(du -sk packages/app/out | cut -f1)
size_mb=$((size_kb / 1024))
echo "Output directory size: ${size_mb} MB (${size_kb} KB)"
if [ "$size_mb" -lt 10 ]; then
echo "::error::Output directory is only ${size_mb} MB, expected at least 10 MB"
exit 1
fi
echo "✓ Output directory size check passed (${size_mb} MB)"

View file

@ -216,3 +216,30 @@ jobs:
tag: TAG
}
});
notify_clickhouse_clickstack:
needs: [check_changesets, release, check_release_app_pushed]
if: needs.check_release_app_pushed.outputs.app_was_pushed == 'true'
timeout-minutes: 5
runs-on: ubuntu-24.04
steps:
- name: Load Environment Variables from .env
uses: xom9ikk/dotenv@v2
- name: Notify ClickHouse/clickhouse-clickstack Downstream
uses: actions/github-script@v7
continue-on-error: true
env:
TAG: ${{ env.IMAGE_VERSION }}${{ env.IMAGE_VERSION_SUB_TAG }}
with:
github-token: ${{ secrets.CH_BOT_PAT }}
script: |
const { TAG } = process.env;
const result = await github.rest.actions.createWorkflowDispatch({
owner: 'ClickHouse',
repo: 'clickhouse-clickstack',
workflow_id: 'sync-hyperdx.yml',
ref: 'main',
inputs: {
tag: TAG,
}
});

3
.gitignore vendored
View file

@ -34,6 +34,7 @@ packages/app/.pnp.js
packages/app/.vercel
packages/app/coverage
packages/app/out
packages/app/tmp
packages/app/next-env.d.ts
# optional npm cache directory
@ -79,4 +80,4 @@ docker-compose.prod.yml
.nx/
# webstorm
.idea
.idea

View file

@ -39,6 +39,8 @@
"app:dev:local": "concurrently -k -n 'APP,COMMON-UTILS' -c 'blue.bold,magenta' 'nx run @hyperdx/app:dev:local' 'nx run @hyperdx/common-utils:dev'",
"app:lint": "nx run @hyperdx/app:ci:lint",
"app:storybook": "nx run @hyperdx/app:storybook",
"build:clickhouse": "nx run @hyperdx/common-utils:build && nx run @hyperdx/app:build:clickhouse",
"run:clickhouse": "nx run @hyperdx/app:run:clickhouse",
"dev": "yarn build:common-utils && dotenvx run --convention=nextjs -- docker compose -f docker-compose.dev.yml up -d && yarn app:dev && docker compose -f docker-compose.dev.yml down",
"dev:local": "IS_LOCAL_APP_MODE='DANGEROUSLY_is_local_app_mode💀' yarn dev",
"dev:down": "docker compose -f docker-compose.dev.yml down",

View file

@ -81,6 +81,14 @@ const nextConfig = {
}
: {}),
}),
...(process.env.NEXT_PUBLIC_CLICKHOUSE_BUILD
? {
assetPrefix: '/clickstack',
basePath: '/clickstack',
images: { unoptimized: true },
output: 'export',
}
: {}),
logging: {
incomingRequests: {
// We also log this in the API server, so we don't want to log it twice.

View file

@ -10,6 +10,8 @@
"dev": "npx dotenv -e .env.development -- next dev --webpack",
"dev:local": "NEXT_PUBLIC_IS_LOCAL_MODE=true npx dotenv -e .env.development -- next dev --webpack",
"build": "next build --webpack",
"build:clickhouse": "NEXT_PUBLIC_THEME=clickstack NEXT_PUBLIC_IS_LOCAL_MODE=true NEXT_PUBLIC_CLICKHOUSE_BUILD=true next build --webpack && node scripts/prepare-clickhouse-build-export.js",
"run:clickhouse": "test -d out && npx rimraf tmp && mkdir tmp && cp -r out tmp/clickstack && echo 'visit http://localhost:3000/clickstack to start' && npx serve tmp -l 3000 || echo 'run build:clickhouse first'",
"start": "next start",
"lint": "npx eslint --quiet . --ext .ts,.tsx",
"lint:fix": "npx eslint . --ext .ts,.tsx --fix",

View file

@ -1,5 +1,6 @@
import { Head, Html, Main, NextScript } from 'next/document';
import { IS_CLICKHOUSE_BUILD } from '@/config';
import { ibmPlexMono, inter, roboto, robotoMono } from '@/fonts';
// Get theme class for SSR - must match ThemeProvider's resolution
@ -28,8 +29,12 @@ export default function Document() {
<Head>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/__ENV.js" />
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/pyodide/pyodide.js"></script>
{!IS_CLICKHOUSE_BUILD && (
<>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/pyodide/pyodide.js"></script>
</>
)}
</Head>
<body>
<Main />

View file

@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
// Move out-gzipped to replace ClickHouse clickstack directory
function moveToClickHouse() {
const outGzippedDir = path.join(__dirname, '../out-gzipped');
const clickStackDir = path.join(
__dirname,
'../../../../ClickHouse/programs/server/clickstack',
);
if (!fs.existsSync(outGzippedDir)) {
console.error('Error: out-gzipped directory does not exist');
console.error('Run build:clickhouse first to generate the build');
process.exit(1);
}
const clickStackParentDir = path.dirname(clickStackDir);
if (!fs.existsSync(clickStackParentDir)) {
console.error(
`Error: ClickHouse server directory does not exist at ${clickStackParentDir}`,
);
console.error(
'Make sure the ClickHouse repository is cloned at ../ClickHouse',
);
process.exit(1);
}
// Remove existing clickstack directory if it exists
if (fs.existsSync(clickStackDir)) {
console.log('Removing existing clickstack directory...');
fs.rmSync(clickStackDir, { recursive: true, force: true });
}
// Copy out-gzipped to clickstack
console.log(`Moving out-gzipped to ${clickStackDir}...`);
fs.cpSync(outGzippedDir, clickStackDir, { recursive: true });
console.log('✓ Successfully moved build to ClickHouse clickstack directory');
}
moveToClickHouse();

View file

@ -0,0 +1,58 @@
const fs = require('fs');
const path = require('path');
const OUT_DIR = path.join(__dirname, '../out');
const PYODIDE_PATH = path.join(OUT_DIR, 'pyodide');
const ALLOWED_EXTENSIONS = [
'.html',
'.js',
'.css',
'.map',
'.woff2',
'.png',
'.svg',
'.ico',
];
// removes pyodide from a next static build. We want a small bundle size, so that feature would just be ignored
if (fs.existsSync(PYODIDE_PATH)) {
fs.rmSync(PYODIDE_PATH, { recursive: true, force: true });
console.log('Removed pyodide from static build');
}
// Remove all files that are not in ALLOWED_EXTENSIONS
function removeNonEssentialFiles(dir) {
let removedCount = 0;
function walkDir(currentPath) {
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
walkDir(fullPath);
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(ext)) {
fs.unlinkSync(fullPath);
console.log(
`Removed non-essential file: ${path.relative(OUT_DIR, fullPath)}`,
);
removedCount++;
}
}
}
}
walkDir(dir);
console.log(`Removed ${removedCount} non-essential file(s)`);
}
// Execute cleanup and optimization
if (fs.existsSync(OUT_DIR)) {
removeNonEssentialFiles(OUT_DIR);
} else {
console.error('No out directory found. Build preparation failed.');
process.exit(1);
}

View file

@ -44,6 +44,7 @@ import {
IconSitemap,
} from '@tabler/icons-react';
import { IS_CLICKHOUSE_BUILD } from '@/config';
import {
useAllFields,
useColumns,
@ -1184,9 +1185,11 @@ const DBSearchPageFiltersComponent = ({
<Text size="xs">Event Deltas</Text>
</Tabs.Tab>
)}
<Tabs.Tab value="pattern" size="xs" h="24px">
<Text size="xs">Event Patterns</Text>
</Tabs.Tab>
{!IS_CLICKHOUSE_BUILD && (
<Tabs.Tab value="pattern" size="xs" h="24px">
<Text size="xs">Event Patterns</Text>
</Tabs.Tab>
)}
</Tabs.List>
</Tabs>

View file

@ -10,7 +10,7 @@ import { notifications } from '@mantine/notifications';
import { IconArrowLeft } from '@tabler/icons-react';
import { ConnectionForm } from '@/components/ConnectionForm';
import { IS_LOCAL_MODE } from '@/config';
import { IS_CLICKHOUSE_BUILD, IS_LOCAL_MODE } from '@/config';
import { useConnections, useCreateConnection } from '@/connection';
import { useMetadataWithSettings } from '@/hooks/useMetadata';
import {
@ -517,6 +517,7 @@ function OnboardingModalComponent({
]);
const handleDemoServerClick = useCallback(async () => {
if (IS_CLICKHOUSE_BUILD) return;
try {
if (sources) {
for (const source of sources) {
@ -724,15 +725,19 @@ function OnboardingModalComponent({
You can always add and edit connections later.
</Text>
)}
<Divider label="OR" my="md" />
<Button
data-testid="demo-server-button"
variant="secondary"
w="100%"
onClick={handleDemoServerClick}
>
Connect to Demo Server
</Button>
{!IS_CLICKHOUSE_BUILD && (
<>
<Divider label="OR" my="md" />
<Button
data-testid="demo-server-button"
variant="secondary"
w="100%"
onClick={handleDemoServerClick}
>
Connect to Demo Server
</Button>
</>
)}
</>
)}
{step === 'auto-detect' && (

View file

@ -27,6 +27,8 @@ export const IS_OSS = process.env.NEXT_PUBLIC_IS_OSS ?? 'true' === 'true';
export const IS_LOCAL_MODE = //true;
// @ts-ignore
(process.env.NEXT_PUBLIC_IS_LOCAL_MODE ?? 'false') === 'true';
export const IS_CLICKHOUSE_BUILD =
process.env.NEXT_PUBLIC_CLICKHOUSE_BUILD === 'true';
// Features in development
export const IS_K8S_DASHBOARD_ENABLED = true;

View file

@ -1,6 +1,9 @@
import React from 'react';
import Link from 'next/link';
import { Box, Center, Text } from '@mantine/core';
import AppNav from '@/components/AppNav';
import { IS_CLICKHOUSE_BUILD } from '@/config';
import { HDXSpotlightProvider } from './Spotlights';
@ -16,10 +19,39 @@ import { HDXSpotlightProvider } from './Spotlights';
export const withAppNav = (page: React.ReactNode) => {
return (
<HDXSpotlightProvider>
<div className="d-flex">
<AppNav fixed />
<div className="w-100 min-w-0" style={{ minWidth: 0 }}>
{page}
<div
className={
IS_CLICKHOUSE_BUILD ? 'app-layout-with-banner' : 'app-layout'
}
>
{IS_CLICKHOUSE_BUILD && (
<Box bg="var(--color-text-primary)">
<Center>
<Text py="xs" size="sm" c="var(--color-text-inverted)">
This is not recommended for production use and is lacking core
ClickStack features such as alerts and saved searches. For a
proper experience, visit the{' '}
<strong>
<Link
href="https://clickhouse.com/docs/use-cases/observability/clickstack/getting-started"
target="_blank"
rel="noopener norefeer"
>
ClickStack Docs
</Link>
</strong>
</Text>
</Center>
</Box>
)}
<div className="d-flex" style={{ height: '100%', overflow: 'hidden' }}>
<AppNav />
<div
className="w-100 min-w-0"
style={{ minWidth: 0, overflow: 'auto' }}
>
{page}
</div>
</div>
</div>
</HDXSpotlightProvider>

View file

@ -12,3 +12,13 @@ a {
* {
box-sizing: border-box;
}
.app-layout {
height: 100vh;
}
.app-layout-with-banner {
height: 100vh;
display: grid;
grid-template-rows: auto 1fr;
}