refactor: Standardize Umami analytics environment variables to non-public names with fallbacks to NEXT_PUBLIC_ prefixed versions.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m51s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m51s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
This commit is contained in:
@@ -27,11 +27,9 @@ function createConfig() {
|
||||
|
||||
analytics: {
|
||||
umami: {
|
||||
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
scriptUrl: env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
|
||||
// The proxied path used in the frontend
|
||||
proxyPath: '/stats/script.js',
|
||||
enabled: Boolean(env.NEXT_PUBLIC_UMAMI_WEBSITE_ID),
|
||||
websiteId: env.UMAMI_WEBSITE_ID,
|
||||
apiEndpoint: env.UMAMI_API_ENDPOINT,
|
||||
enabled: Boolean(env.UMAMI_WEBSITE_ID),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -152,7 +150,7 @@ export function getMaskedConfig() {
|
||||
analytics: {
|
||||
umami: {
|
||||
websiteId: mask(c.analytics.umami.websiteId),
|
||||
scriptUrl: c.analytics.umami.scriptUrl,
|
||||
apiEndpoint: c.analytics.umami.apiEndpoint,
|
||||
enabled: c.analytics.umami.enabled,
|
||||
},
|
||||
},
|
||||
|
||||
13
lib/env.ts
13
lib/env.ts
@@ -15,10 +15,10 @@ export const envSchema = z
|
||||
NEXT_PUBLIC_TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(),
|
||||
|
||||
// Analytics
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess(
|
||||
UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||
UMAMI_API_ENDPOINT: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().default('https://analytics.infra.mintel.me/script.js'),
|
||||
z.string().url().default('https://analytics.infra.mintel.me'),
|
||||
),
|
||||
|
||||
// Error Tracking
|
||||
@@ -82,8 +82,11 @@ export function getRawEnv() {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
|
||||
NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET,
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
|
||||
UMAMI_WEBSITE_ID: process.env.UMAMI_WEBSITE_ID || process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
UMAMI_API_ENDPOINT:
|
||||
process.env.UMAMI_API_ENDPOINT ||
|
||||
process.env.UMAMI_SCRIPT_URL ||
|
||||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
|
||||
SENTRY_DSN: process.env.SENTRY_DSN,
|
||||
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||
MAIL_HOST: process.env.MAIL_HOST,
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service';
|
||||
|
||||
/**
|
||||
* Type definition for the Umami global object.
|
||||
*
|
||||
* This represents the `window.umami` object that the Umami script exposes.
|
||||
* The `track` function can accept either an event name or a URL.
|
||||
*/
|
||||
type UmamiGlobal = {
|
||||
track?: (eventOrUrl: string, props?: AnalyticsEventProperties) => void;
|
||||
};
|
||||
import { config } from '../../config';
|
||||
|
||||
/**
|
||||
* Configuration options for UmamiAnalyticsService.
|
||||
@@ -20,133 +11,90 @@ export type UmamiAnalyticsServiceOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Umami Analytics Service Implementation.
|
||||
* Umami Analytics Service Implementation (Script-less/Proxy edition).
|
||||
*
|
||||
* This service implements the AnalyticsService interface for Umami analytics.
|
||||
* It provides type-safe event tracking and pageview tracking.
|
||||
* This version implements the Umami tracking protocol directly via fetch,
|
||||
* eliminating the need to load an external script.js file.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Service creation (usually done by create-services.ts)
|
||||
* const service = new UmamiAnalyticsService({ enabled: true });
|
||||
*
|
||||
* // Track events
|
||||
* service.track('button_click', { button_id: 'cta' });
|
||||
* service.trackPageview('/products/123');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Using through the service layer (recommended)
|
||||
* import { getAppServices } from '@/lib/services/create-services';
|
||||
*
|
||||
* const services = getAppServices();
|
||||
* services.analytics.track('product_add_to_cart', {
|
||||
* product_id: '123',
|
||||
* price: 99.99,
|
||||
* });
|
||||
* ```
|
||||
* In the browser, it gathers standard metadata (screen, language, referrer)
|
||||
* and sends it to the proxied '/stats/api/send' endpoint.
|
||||
*/
|
||||
export class UmamiAnalyticsService implements AnalyticsService {
|
||||
constructor(private readonly options: UmamiAnalyticsServiceOptions) {}
|
||||
private websiteId?: string;
|
||||
private endpoint: string;
|
||||
|
||||
constructor(private readonly options: UmamiAnalyticsServiceOptions) {
|
||||
this.websiteId = config.analytics.umami.websiteId;
|
||||
|
||||
// On server, use the full internal URL; on client, use the proxied path
|
||||
this.endpoint = typeof window === 'undefined' ? config.analytics.umami.apiEndpoint : '/stats';
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a custom event with optional properties.
|
||||
*
|
||||
* This method checks if analytics are enabled and if we're in a browser environment
|
||||
* before attempting to track the event.
|
||||
*
|
||||
* @param eventName - The name of the event to track
|
||||
* @param props - Optional event properties
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* service.track('product_add_to_cart', {
|
||||
* product_id: '123',
|
||||
* product_name: 'Cable',
|
||||
* price: 99.99,
|
||||
* quantity: 1,
|
||||
* });
|
||||
* ```
|
||||
* Internal method to send the payload to Umami API.
|
||||
*/
|
||||
private async sendPayload(type: 'event', data: Record<string, any>) {
|
||||
if (!this.options.enabled || !this.websiteId) return;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
website: this.websiteId,
|
||||
hostname: typeof window !== 'undefined' ? window.location.hostname : 'server',
|
||||
screen:
|
||||
typeof window !== 'undefined'
|
||||
? `${window.screen.width}x${window.screen.height}`
|
||||
: undefined,
|
||||
language: typeof window !== 'undefined' ? navigator.language : undefined,
|
||||
referrer: typeof window !== 'undefined' ? document.referrer : undefined,
|
||||
...data,
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.endpoint}/api/send`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': typeof window === 'undefined' ? 'KLZ-Server' : navigator.userAgent,
|
||||
},
|
||||
body: JSON.stringify({ type, payload }),
|
||||
// Use keepalive for page navigation events to ensure they complete
|
||||
keepalive: true,
|
||||
} as any);
|
||||
|
||||
if (!response.ok && process.env.NODE_ENV === 'development') {
|
||||
const errorText = await response.text();
|
||||
console.warn(`[Umami] API responded with ${response.status}: ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('[Umami] Failed to send analytics:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a custom event.
|
||||
*/
|
||||
track(eventName: string, props?: AnalyticsEventProperties) {
|
||||
if (!this.options.enabled) return;
|
||||
|
||||
// Server-side tracking via proxy
|
||||
if (typeof window === 'undefined') {
|
||||
const { getServerAppServices } = require('../create-services.server');
|
||||
const { config } = require('../../config');
|
||||
const websiteId = config.analytics.umami.websiteId;
|
||||
const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', '');
|
||||
|
||||
if (!websiteId) return;
|
||||
|
||||
const logger = getServerAppServices().logger.child({ component: 'analytics' });
|
||||
logger.info('Sending analytics event', { eventName, props });
|
||||
|
||||
fetch(`${umamiUrl}/api/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
|
||||
body: JSON.stringify({ type: 'event', payload: { website: websiteId, name: eventName, data: props } }),
|
||||
}).catch((error) => {
|
||||
logger.error('Failed to send analytics event', { eventName, props, error });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const umami = (window as unknown as { umami?: UmamiGlobal }).umami;
|
||||
umami?.track?.(eventName, props);
|
||||
this.sendPayload('event', {
|
||||
name: eventName,
|
||||
data: props,
|
||||
url:
|
||||
typeof window !== 'undefined'
|
||||
? window.location.pathname + window.location.search
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a pageview.
|
||||
*
|
||||
* This method checks if analytics are enabled and if we're in a browser environment
|
||||
* before attempting to track the pageview.
|
||||
*
|
||||
* Umami treats `track(url)` as a pageview override, so we can use the same
|
||||
* `track` function for both events and pageviews.
|
||||
*
|
||||
* @param url - The URL to track (defaults to current location)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Track current page
|
||||
* service.trackPageview();
|
||||
*
|
||||
* // Track custom URL
|
||||
* service.trackPageview('/products/123?category=cables');
|
||||
* ```
|
||||
*/
|
||||
trackPageview(url?: string) {
|
||||
if (!this.options.enabled) return;
|
||||
|
||||
// Server-side tracking via proxy
|
||||
if (typeof window === 'undefined') {
|
||||
const { getServerAppServices } = require('../create-services.server');
|
||||
const { config } = require('../../config');
|
||||
const websiteId = config.analytics.umami.websiteId;
|
||||
const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', '');
|
||||
|
||||
if (!websiteId || !url) return;
|
||||
|
||||
const logger = getServerAppServices().logger.child({ component: 'analytics' });
|
||||
logger.info('Sending analytics pageview', { url });
|
||||
|
||||
fetch(`${umamiUrl}/api/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' },
|
||||
body: JSON.stringify({ type: 'event', payload: { website: websiteId, url } }),
|
||||
}).catch((error) => {
|
||||
logger.error('Failed to send analytics pageview', { url, error });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const umami = (window as unknown as { umami?: UmamiGlobal }).umami;
|
||||
|
||||
// Umami treats `track(url)` as a pageview override.
|
||||
if (url) umami?.track?.(url);
|
||||
else umami?.track?.(window.location.pathname + window.location.search);
|
||||
this.sendPayload('event', {
|
||||
url:
|
||||
url ||
|
||||
(typeof window !== 'undefined'
|
||||
? window.location.pathname + window.location.search
|
||||
: undefined),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ let singleton: AppServices | undefined;
|
||||
* - Cache service (in-memory)
|
||||
*
|
||||
* The services are configured based on environment variables:
|
||||
* - `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Enables Umami analytics
|
||||
* - `UMAMI_WEBSITE_ID` - Enables Umami analytics
|
||||
* - `NEXT_PUBLIC_SENTRY_DSN` - Enables client-side error reporting
|
||||
* - `SENTRY_DSN` - Enables server-side error reporting
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user