mirror of
https://github.com/hanzoai/analytics.git
synced 2026-10-30 15:05:29 +00:00
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.
230 lines
5.4 KiB
JavaScript
230 lines
5.4 KiB
JavaScript
// 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);
|