Files
analytics/next.config.mjs
zeekay afbb7243d6 A website's page is the shared analytics views
The page mounts @hanzo/dashboard's WebAnalytics and ProductAnalytics,
the same components a project's page in the platform mounts, over this
app's /v1 API. One GuiProvider in Providers serves them and the sign-in.
Tokens minted for the first-party consoles are accepted too
(IAM_AUDIENCES), so the platform reads this API with its viewer's own
bearer.
2026-09-30 07:31:42 -07:00

230 lines
5.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Plain .mjs, NOT next.config.ts — deliberately.
//
// Next loads a TypeScript config through the TypeScript compiler API, and this
// repo is on typescript ^7 (the new native compiler), whose API surface differs.
// The build died before compiling anything:
//
// ⨯ Failed to load next.config.ts
// [TypeError: Cannot read properties of undefined (reading 'fileExists')]
//
// which reads as a Next bug and is really a config-loader/toolchain mismatch.
// Loading the config must not depend on which TypeScript is installed.
//
// This is a pure RENAME: the file contained no TypeScript-only syntax — no type
// annotations, no interfaces, no assertions — so it was already plain ESM that
// happened to carry a .ts extension. Nothing in it changes.
//
// hanzoai/console is on the same typescript ^7 and Next 15 line, already uses
// next.config.mjs, and builds clean. TypeScript 7 stays for the application
// source; only config LOADING is decoupled from it.
import 'dotenv/config';
import pkg from './package.json' with { type: 'json' };
import withGui from '@hanzo/ui/next';
const TRACKER_SCRIPT = '/script.js';
const basePath = process.env.BASE_PATH || '';
const cloudMode = process.env.CLOUD_MODE || '';
const cloudUrl = process.env.CLOUD_URL || '';
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
const corsMaxAge = process.env.CORS_MAX_AGE || '';
const defaultLocale = process.env.DEFAULT_LOCALE || '';
const forceSSL = process.env.FORCE_SSL || '';
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
const contentSecurityPolicy = `
default-src 'self';
img-src 'self' https: data:;
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' https:;
frame-ancestors 'self' ${frameAncestors};
`;
const defaultHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'Content-Security-Policy',
value: contentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
},
];
if (forceSSL) {
defaultHeaders.push({
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
});
}
const trackerHeaders = [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Cache-Control',
value: 'public, max-age=86400, must-revalidate',
},
];
const apiHeaders = [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Access-Control-Allow-Headers',
value: '*',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, DELETE, POST, PUT',
},
{
key: 'Access-Control-Max-Age',
value: corsMaxAge || '86400',
},
{
key: 'Cache-Control',
value: 'no-cache',
},
];
const headers = [
{
// Every route this app serves is under /v1, and the tracker posts to
// /v1/send cross-origin from the sites it measures.
source: '/v1/:path*',
headers: apiHeaders,
},
{
source: '/:path*',
headers: defaultHeaders,
},
{
source: TRACKER_SCRIPT,
headers: trackerHeaders,
},
];
const rewrites = [];
if (trackerScriptURL) {
rewrites.push({
source: TRACKER_SCRIPT,
destination: trackerScriptURL,
});
}
if (collectApiEndpoint) {
headers.push({
source: collectApiEndpoint,
headers: apiHeaders,
});
rewrites.push({
source: collectApiEndpoint,
destination: '/v1/send',
});
}
const redirects = [
{
source: '/settings',
destination: '/settings/preferences',
permanent: false,
},
{
source: '/teams/:id',
destination: '/teams/:id/websites',
permanent: false,
},
{
source: '/teams/:id/settings',
destination: '/teams/:id/settings/preferences',
permanent: false,
},
{
source: '/admin',
destination: '/admin/users',
permanent: false,
},
];
// Adding rewrites + headers for all alternative tracker script names.
if (trackerScriptName) {
const names = trackerScriptName?.split(',').map(name => name.trim());
if (names) {
names.forEach(name => {
const normalizedSource = `/${name.replace(/^\/+/, '')}`;
rewrites.push({
source: normalizedSource,
destination: TRACKER_SCRIPT,
});
headers.push({
source: normalizedSource,
headers: trackerHeaders,
});
});
}
}
if (cloudMode) {
rewrites.push({
source: '/script.js',
destination: cloudUrl ? `${cloudUrl}/script.js` : '/script.js',
});
}
// The login renders @hanzo/ui's SignIn, which draws through @hanzo/gui; withGui
// is the one place a Next host learns to build that graph for the web.
/** @type {import('next').NextConfig} */
export default withGui({
reactStrictMode: false,
env: {
basePath,
cloudMode,
cloudUrl,
currentVersion: pkg.version,
defaultLocale,
},
basePath,
output: 'standalone',
// The shared analytics views ship as TypeScript source.
transpilePackages: ['@hanzo/dashboard'],
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
async headers() {
return headers;
},
async rewrites() {
return [
...rewrites,
{
source: '/telemetry.js',
destination: '/v1/scripts/telemetry',
},
{
source: '/teams/:teamId/:path*',
destination: '/:path*',
},
];
},
async redirects() {
return [...redirects];
},
}, import.meta.dirname);