mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +08:00
## Summary
Adds richer analytics overview metrics and filterable dashboard
breakdowns.
- adds hourly overview series for the 1-day range
- adds country, referrer, browser, OS, and device filters to internal
metrics
- adds bounce rate, session duration, top countries, top browsers, top
operating systems, and device breakdowns
- updates the overview dashboard with filter chips, top-list cards,
animated metric states, and 1-day hourly chart support
- captures user agent on page-view analytics events, with a server-side
fallback for older clients
## Validation
Attempted targeted tests:
`pnpm test run
apps/backend/src/app/api/latest/internal/metrics/route.test.ts
'apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/analytics-chart-mode.test.ts'`
This did not reach Vitest in the temporary split worktree because
`node_modules` is not installed there and the repo pre-step failed at
`pnpm exec tsx ./scripts/generate-sdks.ts`.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds analytics overview filters with optional date‑range bounds and
1‑day hourly charts, plus smoother, accessible animations across charts
and top lists. Improves correctness and stability with deterministic
caching, normalized inputs, client‑only user‑agent capture, and
globe/layout fixes.
- **New Features**
- Filterable analytics overview (country, referrer, browser, OS, device)
with normalized inputs and optional `since`/`until`; API/admin/dashboard
accept `AnalyticsOverviewFilters` with deterministic cache keys.
- 1‑day hourly charts (page views, visitors) and a metric mode toggle
(DAU, Visitors, Revenue); animated top‑lists and sparklines powered by
`motion` with reduced‑motion support.
- UI: filter chips/menu, clearer tooltips (incl. user metric cards),
optional interactive globe with dynamic camera distance; exported
`TooltipPortal` from `@hexclave/ui`.
- **Refactors & Bug Fixes**
- Event ingest: client sends `user_agent`; removed server‑side fallback;
added user‑agent filter‑fragment builder and tests.
- Metrics correctness: aligned hourly bounds to start of UTC hour;
derived 1‑day revenue total from daily series; resilient chart x‑axis
formatting; country filter options use analytics `top_regions`;
fixed‑'en' locale for top‑lists; added date‑range parsing/validation for
filters.
- UI/runtime: smoother pill/tab slider animations with guards for
missing Web APIs; added `containedHeight` to `PageLayout` and wired into
sidebar/session replays; globe disables zoom when non‑interactive.
- Misc: instrumentation runs only in Node (`process.env.NEXT_RUNTIME ===
"nodejs"`); analytics/overview page redirects with URL‑encoded
`projectId`; Docker: include `@hexclave/template` in `turbo prune` to
fix CI builds.
<sup>Written for commit 7fcd3558a5.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1496?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Analytics filters (country, referrer, browser, OS, device); hourly
signup and active-user series; expanded hourly/daily analytics payloads
and top-lists UI.
* Chart metric modes (DAU, Visitors, Revenue), optional page-views
series, interactive globe support, animated Top Lists, and sparkline
animations.
* **Improvements**
* Better user-agent capture/normalization for batched events and
page-view tracking; reduced-motion aware animations; enhanced tooltips
and UI slider/tab indicators.
* Added motion library dependency.
* **Tests**
* New unit tests for analytics filters and chart metric mode behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mantra <mantra@stack-auth.com>
294 lines
12 KiB
Docker
294 lines
12 KiB
Docker
# Hexclave Local Emulator — All-in-One Image
|
|
# Packages: PostgreSQL 16, Redis 7, Inbucket, Svix, ClickHouse, MinIO, QStash
|
|
# + built Hexclave backend and dashboard
|
|
|
|
ARG NODE_VERSION=22.21.1
|
|
|
|
# ── Node.js build stages ──────────────────────────────────────────────────────
|
|
|
|
FROM node:${NODE_VERSION} AS node-base
|
|
|
|
WORKDIR /app
|
|
|
|
RUN apt-get update && \
|
|
apt-get upgrade -y && \
|
|
rm -rf /var/lib/apt/lists
|
|
|
|
ENV PNPM_HOME=/pnpm
|
|
ENV PATH=$PNPM_HOME:$PNPM_HOME/bin:$PATH
|
|
|
|
RUN corepack enable
|
|
RUN corepack prepare pnpm@11.5.0 --activate
|
|
RUN pnpm add -g turbo
|
|
RUN pnpm add -g tsx
|
|
|
|
|
|
FROM node-base AS pruner
|
|
|
|
COPY . .
|
|
|
|
RUN tsx ./scripts/generate-sdks.ts
|
|
|
|
# https://turbo.build/repo/docs/guides/tools/docker
|
|
RUN turbo prune --scope=@hexclave/backend --scope=@hexclave/dashboard --scope=@hexclave/template --docker
|
|
|
|
|
|
FROM node-base AS builder
|
|
|
|
# Skip generate-sdks.ts in preinstall hook (file not available in pruned output)
|
|
ENV HEXCLAVE_SKIP_TEMPLATE_GENERATION=true
|
|
|
|
# copy over package.json files and install dependencies
|
|
COPY --from=pruner /app/out/json/ .
|
|
COPY --from=pruner /app/out/pnpm-lock.yaml .
|
|
COPY .gitignore .
|
|
COPY pnpm-workspace.yaml .
|
|
COPY turbo.json .
|
|
COPY configs ./configs
|
|
COPY --from=pruner /app/scripts/postinstall-patch-next-async-debug-info.mjs ./scripts/
|
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|
|
|
# copy over the rest of the code for the build
|
|
COPY --from=pruner /app/out/full/ .
|
|
|
|
# docs are currently required for the NextJS backend build, but won't exist in the final image
|
|
COPY docs ./docs
|
|
|
|
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
|
ENV NEXT_CONFIG_OUTPUT=standalone
|
|
ENV NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator
|
|
|
|
# Build the backend NextJS app
|
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm turbo run docker-build --filter=@hexclave/backend... --filter=@hexclave/dashboard...
|
|
|
|
# Build the self-host seed script.
|
|
# tsdown -> rolldown is multi-threaded Rust; under qemu-user (cross-arch
|
|
# arm64-on-amd64) its futex emulation occasionally deadlocks and the build
|
|
# hangs forever. Bound each attempt and retry to ride out the race.
|
|
RUN cd apps/backend && \
|
|
attempt=1; \
|
|
while :; do \
|
|
timeout --kill-after=30s 600s pnpm build-self-host-migration-script && break; \
|
|
rc=$?; \
|
|
if [ "$attempt" -ge 3 ]; then \
|
|
echo "build-self-host-migration-script failed after $attempt attempts (last rc=$rc)" >&2; \
|
|
exit "$rc"; \
|
|
fi; \
|
|
echo "build-self-host-migration-script attempt $attempt failed (rc=$rc); retrying..." >&2; \
|
|
attempt=$((attempt + 1)); \
|
|
done
|
|
|
|
|
|
# Prune node_modules for runtime: remove dev tools, heavy UI packages,
|
|
# duplicate framework copies, and native binaries not needed by the
|
|
# migration script or server at runtime.
|
|
FROM builder AS migration-pruner
|
|
RUN cp -a /app/node_modules /pruned-node_modules && \
|
|
cd /pruned-node_modules/.pnpm && \
|
|
rm -rf \
|
|
# Dev tools (never needed at runtime)
|
|
typescript@* eslint@* eslint-*@* @typescript-eslint+*@* \
|
|
prettier@* vitest@* jsdom@* turbo@* turbo-*@* \
|
|
tsdown@* @changesets+*@* codebuff@* \
|
|
@testing-library+*@* vite@* vite-*@* @vitejs+*@* \
|
|
# Heavy UI packages (already traced into Next.js standalone bundles)
|
|
monaco-editor@* \
|
|
three@* three-globe@* globe.gl@* react-globe*@* \
|
|
react-icons@* lucide-react@* @phosphor-icons+*@* \
|
|
# Large optional packages not needed by migration script
|
|
posthog-js@* \
|
|
@prisma+studio-core@* @prisma+dev@* @prisma+query-plan-executor@* \
|
|
convex@* @electric-sql+*@* \
|
|
next@14* @next+swc-*@14* \
|
|
# Native build binaries not needed at runtime
|
|
@esbuild+*@* esbuild@* @rolldown+*@* \
|
|
# Duplicate date-fns versions (keep v4 only)
|
|
date-fns@2* date-fns@3*
|
|
|
|
|
|
# ── Freestyle mock build ─────────────────────────────────────────────────────
|
|
|
|
FROM node-base AS freestyle-mock-builder
|
|
WORKDIR /freestyle-mock
|
|
COPY docker/dependencies/freestyle-mock/Dockerfile /tmp/freestyle-mock-dockerfile
|
|
# Extract the inline package.json and server.mjs from the Dockerfile's RUN cat commands,
|
|
# then install dependencies. This avoids duplicating the source.
|
|
RUN node -e " \
|
|
const fs = require('fs'); \
|
|
const df = fs.readFileSync('/tmp/freestyle-mock-dockerfile', 'utf8'); \
|
|
const pkgMatch = df.match(/cat <<'EOF' > package\\.json\\n([\\s\\S]*?)\\nEOF/); \
|
|
fs.writeFileSync('package.json', pkgMatch[1]); \
|
|
const srvMatch = df.match(/cat <<'EOF' > server\\.mjs\\n([\\s\\S]*?)\\nEOF/); \
|
|
let server = srvMatch[1]; \
|
|
server = server.replace( \
|
|
'from \"fs/promises\"', \
|
|
'from \"fs/promises\"; import { symlinkSync } from \"fs\"' \
|
|
); \
|
|
server = server.replace( \
|
|
'await mkdir(workDir, { recursive: true });', \
|
|
'await mkdir(workDir, { recursive: true }); try { symlinkSync(\"/app/freestyle-mock/node_modules\", join(workDir, \"node_modules\")); } catch {}' \
|
|
); \
|
|
fs.writeFileSync('server.mjs', server); \
|
|
"
|
|
RUN npm install
|
|
|
|
|
|
# ── Mock OAuth server build ───────────────────────────────────────────────────
|
|
|
|
FROM node-base AS mock-oauth-builder
|
|
WORKDIR /mock-oauth
|
|
COPY apps/mock-oauth-server/package.json .
|
|
RUN printf 'allowBuilds:\n esbuild: true\n' > pnpm-workspace.yaml && pnpm install && pnpm add esbuild --save-dev
|
|
COPY apps/mock-oauth-server/src ./src
|
|
RUN npx esbuild src/index.ts --bundle --platform=node --target=node22 --outfile=dist/index.cjs
|
|
|
|
|
|
# ── Service binary stages ─────────────────────────────────────────────────────
|
|
|
|
FROM stripe/stripe-mock:v0.195.0 AS stripe-mock-bin
|
|
FROM inbucket/inbucket:3.1.0 AS inbucket-bin
|
|
FROM svix/svix-server:v1.88.0 AS svix-bin
|
|
FROM clickhouse/clickhouse-server:25.10 AS clickhouse-bin
|
|
FROM minio/minio:RELEASE.2025-09-07T16-13-09Z AS minio-bin
|
|
FROM minio/mc:RELEASE.2025-02-21T16-00-46Z AS mc-bin
|
|
|
|
FROM bgodil/qstash:latest AS qstash-bin
|
|
RUN cp $(which qstash) /qstash-binary 2>/dev/null || \
|
|
cp $(find / -name 'qstash' -type f -executable 2>/dev/null | head -1) /qstash-binary || \
|
|
{ echo "ERROR: qstash binary not found" >&2; exit 1; }
|
|
|
|
|
|
# ── Strip / compress service binaries (parallel stages) ──────────────────────
|
|
|
|
FROM debian:trixie-slim AS upx-compress
|
|
RUN apt-get update && apt-get install -y --no-install-recommends upx-ucl binutils && \
|
|
rm -rf /var/lib/apt/lists/*
|
|
COPY --from=clickhouse-bin /usr/bin/clickhouse /out/clickhouse
|
|
COPY --from=svix-bin /usr/local/bin/svix-server /out/svix-server
|
|
COPY --from=minio-bin /usr/bin/minio /out/minio
|
|
COPY --from=mc-bin /usr/bin/mc /out/mc
|
|
COPY --from=qstash-bin /qstash-binary /out/qstash
|
|
RUN chmod u+w /out/* && \
|
|
# Intentionally NOT stripping /out/clickhouse. The clickhouse binary is a
|
|
# self-extracting compressed executable (a small loader with a ZSTD
|
|
# payload appended after the section table); strip rewrites the ELF and
|
|
# can invalidate the loader's "find my payload" lookup, causing the
|
|
# decompressor to spin on garbage with zero log output — the exact
|
|
# symptom seen on cross-arch TCG runs. Savings from stripping would be
|
|
# only the tiny bootstrap anyway since the payload isn't in any section.
|
|
strip --strip-all /out/minio /out/svix-server /out/mc /out/qstash && \
|
|
upx -9 /out/minio /out/svix-server /out/mc /out/qstash
|
|
|
|
|
|
# ── Final image ───────────────────────────────────────────────────────────────
|
|
|
|
FROM debian:trixie-slim
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
RUN apt-get update && \
|
|
apt-get install -y --no-install-recommends \
|
|
gnupg2 \
|
|
lsb-release \
|
|
curl \
|
|
ca-certificates \
|
|
&& echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
|
|
> /etc/apt/sources.list.d/pgdg.list \
|
|
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
|
| gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
postgresql-16 \
|
|
postgresql-client-16 \
|
|
redis-server \
|
|
supervisor \
|
|
gosu \
|
|
procps \
|
|
libssl3 \
|
|
openssl \
|
|
socat \
|
|
&& apt-get purge -y --auto-remove gnupg2 lsb-release \
|
|
&& rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man /usr/share/i18n
|
|
|
|
# Node.js runtime (binary only — app bundles include all JS dependencies)
|
|
COPY --from=node-base /usr/local/bin/node /usr/local/bin/node
|
|
|
|
# Inbucket
|
|
COPY --from=inbucket-bin /opt/inbucket /opt/inbucket
|
|
|
|
# Stripe mock
|
|
COPY --from=stripe-mock-bin /bin/stripe-mock /usr/local/bin/stripe-mock
|
|
|
|
# Svix (UPX-compressed)
|
|
COPY --from=upx-compress /out/svix-server /usr/local/bin/svix-server
|
|
|
|
# ClickHouse (stripped only)
|
|
COPY --from=upx-compress /out/clickhouse /usr/bin/clickhouse
|
|
RUN ln -sf /usr/bin/clickhouse /usr/bin/clickhouse-server && \
|
|
ln -sf /usr/bin/clickhouse /usr/bin/clickhouse-client
|
|
|
|
# MinIO (UPX-compressed)
|
|
COPY --from=upx-compress /out/minio /usr/local/bin/minio
|
|
COPY --from=upx-compress /out/mc /usr/local/bin/mc
|
|
|
|
# QStash (UPX-compressed)
|
|
COPY --from=upx-compress --chmod=755 /out/qstash /usr/local/bin/qstash
|
|
|
|
# App
|
|
WORKDIR /app
|
|
COPY --from=builder /app/apps/backend/.next/standalone ./
|
|
COPY --from=builder /app/apps/backend/.next/static ./apps/backend/.next/static
|
|
COPY --from=builder /app/apps/backend/prisma ./apps/backend/prisma
|
|
COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
|
|
COPY --from=builder /app/apps/backend/node_modules ./apps/backend/node_modules
|
|
COPY --from=builder /app/apps/dashboard/.next/standalone ./
|
|
COPY --from=builder /app/apps/dashboard/.next/static ./apps/dashboard/.next/static
|
|
COPY --from=builder /app/apps/dashboard/public ./apps/dashboard/public
|
|
# Save the standalone-traced node_modules (runtime deps only) before the full
|
|
# migration-pruner copy overwrites it. The slim-docker-image step in the QEMU
|
|
# build restores this after migrations are baked in.
|
|
RUN cp -a /app/node_modules /app/node_modules.standalone 2>/dev/null || mkdir -p /app/node_modules.standalone
|
|
COPY --from=migration-pruner /pruned-node_modules ./node_modules
|
|
COPY --from=builder /app/packages ./packages
|
|
|
|
# Mock OAuth server (bundled single file)
|
|
COPY --from=mock-oauth-builder /mock-oauth/dist/index.cjs /app/mock-oauth-server/index.cjs
|
|
|
|
# Freestyle mock (JS execution for email rendering)
|
|
COPY --from=freestyle-mock-builder /freestyle-mock /app/freestyle-mock
|
|
COPY --from=node-base /usr/local/bin/npm /usr/local/bin/npm
|
|
COPY --from=node-base /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm
|
|
|
|
RUN mkdir -p \
|
|
/data/postgres \
|
|
/data/redis \
|
|
/data/clickhouse \
|
|
/data/clickhouse/access \
|
|
/data/clickhouse/tmp \
|
|
/data/clickhouse/user_files \
|
|
/data/clickhouse/format_schemas \
|
|
/data/minio \
|
|
/data/inbucket \
|
|
/var/log/supervisor \
|
|
/var/log/clickhouse \
|
|
/etc/clickhouse-server \
|
|
&& chown -R postgres:postgres /data/postgres
|
|
|
|
COPY docker/local-emulator/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
|
COPY docker/local-emulator/run-cron-jobs.sh /run-cron-jobs.sh
|
|
COPY docker/local-emulator/entrypoint.sh /entrypoint.sh
|
|
COPY docker/local-emulator/init-services.sh /init-services.sh
|
|
COPY docker/local-emulator/start-app.sh /start-app.sh
|
|
COPY docker/local-emulator/rotate-secrets.sh /usr/local/bin/rotate-secrets
|
|
COPY docker/local-emulator/clickhouse-config.xml /etc/clickhouse-server/config.xml
|
|
COPY docker/local-emulator/clickhouse-users.xml /etc/clickhouse-server/users.xml
|
|
COPY docker/server/entrypoint.sh /app-entrypoint.sh
|
|
RUN chmod +x /entrypoint.sh /init-services.sh /start-app.sh /app-entrypoint.sh /run-cron-jobs.sh /usr/local/bin/rotate-secrets
|
|
|
|
# PostgreSQL: 5432, Redis: 6379, Inbucket: 2500/9001/1100,
|
|
# Svix: 8071, ClickHouse: 8123/9009, MinIO: 9090, QStash: 8080
|
|
# Backend: 8102, Dashboard: 8101, Mock OAuth: 8114
|
|
EXPOSE 5432 6379 2500 9001 1100 8071 8123 9009 9090 8080 8101 8102 8114
|
|
|
|
ENTRYPOINT ["/entrypoint.sh"]
|