diff --git a/apps/sample-website/package.json b/apps/sample-website/package.json
index e31fcdf..ea50b4d 100644
--- a/apps/sample-website/package.json
+++ b/apps/sample-website/package.json
@@ -22,6 +22,9 @@
},
"dependencies": {
"@mintel/next-utils": "workspace:*",
+ "@mintel/observability": "workspace:*",
+ "@mintel/next-observability": "workspace:*",
+ "@sentry/nextjs": "^8.55.0",
"next": "15.1.6",
"next-intl": "^4.8.2",
"react": "^19.0.0",
diff --git a/apps/sample-website/sentry.client.config.ts b/apps/sample-website/sentry.client.config.ts
new file mode 100644
index 0000000..bf6033d
--- /dev/null
+++ b/apps/sample-website/sentry.client.config.ts
@@ -0,0 +1,9 @@
+import { initSentry } from "@mintel/next-observability";
+
+initSentry({
+ // Use a placeholder DSN on the client if you want to bypass ad-blockers via tunnel
+ // Or just use the real DSN if you don't care about ad-blockers for errors.
+ // The Mintel standard is to use the relay.
+ dsn: "https://public@errors.infra.mintel.me/1", // Placeholder for relay
+ tunnel: "/errors/api/relay",
+});
diff --git a/apps/sample-website/sentry.edge.config.ts b/apps/sample-website/sentry.edge.config.ts
new file mode 100644
index 0000000..6b943bc
--- /dev/null
+++ b/apps/sample-website/sentry.edge.config.ts
@@ -0,0 +1,8 @@
+import { initSentry } from "@mintel/next-observability";
+import { validateMintelEnv } from "@mintel/next-utils";
+
+const env = validateMintelEnv();
+
+initSentry({
+ dsn: env.SENTRY_DSN,
+});
diff --git a/apps/sample-website/sentry.server.config.ts b/apps/sample-website/sentry.server.config.ts
new file mode 100644
index 0000000..6b943bc
--- /dev/null
+++ b/apps/sample-website/sentry.server.config.ts
@@ -0,0 +1,8 @@
+import { initSentry } from "@mintel/next-observability";
+import { validateMintelEnv } from "@mintel/next-utils";
+
+const env = validateMintelEnv();
+
+initSentry({
+ dsn: env.SENTRY_DSN,
+});
diff --git a/apps/sample-website/src/app/errors/api/relay/route.ts b/apps/sample-website/src/app/errors/api/relay/route.ts
new file mode 100644
index 0000000..80b734a
--- /dev/null
+++ b/apps/sample-website/src/app/errors/api/relay/route.ts
@@ -0,0 +1,6 @@
+import { createSentryRelayHandler } from "@mintel/next-observability";
+import { validateMintelEnv } from "@mintel/next-utils";
+
+export const POST = createSentryRelayHandler({
+ dsn: validateMintelEnv().SENTRY_DSN,
+});
diff --git a/apps/sample-website/src/app/layout.tsx b/apps/sample-website/src/app/layout.tsx
index 7b7c69c..ac41ea6 100644
--- a/apps/sample-website/src/app/layout.tsx
+++ b/apps/sample-website/src/app/layout.tsx
@@ -1,5 +1,11 @@
import type { Metadata } from "next";
+import { Suspense } from "react";
import "./globals.css";
+import {
+ AnalyticsContextProvider,
+ AnalyticsAutoTracker,
+} from "@mintel/next-observability/client";
+import { getAnalyticsConfig } from "@/lib/observability";
export const metadata: Metadata = {
title: "Sample Website",
@@ -11,9 +17,18 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
+ const analyticsConfig = getAnalyticsConfig();
+
return (
-
{children}
+
+
+
+
+
+ {children}
+
+
);
}
diff --git a/apps/sample-website/src/app/stats/api/send/route.ts b/apps/sample-website/src/app/stats/api/send/route.ts
new file mode 100644
index 0000000..cd0d0cd
--- /dev/null
+++ b/apps/sample-website/src/app/stats/api/send/route.ts
@@ -0,0 +1,7 @@
+import { createUmamiProxyHandler } from "@mintel/next-observability";
+import { validateMintelEnv } from "@mintel/next-utils";
+
+export const POST = createUmamiProxyHandler({
+ websiteId: validateMintelEnv().UMAMI_WEBSITE_ID,
+ apiEndpoint: validateMintelEnv().UMAMI_API_ENDPOINT,
+});
diff --git a/apps/sample-website/src/instrumentation.ts b/apps/sample-website/src/instrumentation.ts
new file mode 100644
index 0000000..ecb6528
--- /dev/null
+++ b/apps/sample-website/src/instrumentation.ts
@@ -0,0 +1,13 @@
+import * as Sentry from "@sentry/nextjs";
+
+export async function register() {
+ if (process.env.NEXT_RUNTIME === "nodejs") {
+ await import("../sentry.server.config");
+ }
+
+ if (process.env.NEXT_RUNTIME === "edge") {
+ await import("../sentry.edge.config");
+ }
+}
+
+export const onRequestError = Sentry.captureRequestError;
diff --git a/apps/sample-website/src/lib/observability.ts b/apps/sample-website/src/lib/observability.ts
new file mode 100644
index 0000000..b2a82e6
--- /dev/null
+++ b/apps/sample-website/src/lib/observability.ts
@@ -0,0 +1,54 @@
+import {
+ UmamiAnalyticsService,
+ GotifyNotificationService,
+ NoopNotificationService,
+} from "@mintel/observability";
+import { validateMintelEnv } from "@mintel/next-utils";
+
+let analyticsService: any = null;
+let notificationService: any = null;
+
+export function getAnalyticsConfig() {
+ const isClient = typeof window !== "undefined";
+
+ if (isClient) {
+ return {
+ enabled: true,
+ apiEndpoint: "/stats",
+ };
+ }
+
+ const env = validateMintelEnv();
+ return {
+ enabled: Boolean(env.UMAMI_WEBSITE_ID),
+ websiteId: env.UMAMI_WEBSITE_ID,
+ apiEndpoint: env.UMAMI_API_ENDPOINT,
+ };
+}
+
+export function getAnalyticsService() {
+ if (analyticsService) return analyticsService;
+
+ const config = getAnalyticsConfig();
+ analyticsService = new UmamiAnalyticsService(config);
+
+ return analyticsService;
+}
+
+export function getNotificationService() {
+ if (notificationService) return notificationService;
+
+ if (typeof window === "undefined") {
+ const env = validateMintelEnv();
+ notificationService = new GotifyNotificationService({
+ enabled: Boolean(env.GOTIFY_URL && env.GOTIFY_TOKEN),
+ url: env.GOTIFY_URL || "",
+ token: env.GOTIFY_TOKEN || "",
+ });
+ } else {
+ // Notifications are typically server-side only to protect tokens
+ notificationService = new NoopNotificationService();
+ }
+
+ return notificationService;
+}
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index a9afb73..35b7811 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -125,6 +125,7 @@ program
react: "^19.0.0",
"react-dom": "^19.0.0",
"@mintel/next-utils": "workspace:*",
+ "@mintel/next-observability": "workspace:*",
"@directus/sdk": "^21.0.0",
},
devDependencies: {
@@ -263,11 +264,15 @@ export default createMintelI18nRequestConfig(
// Create instrumentation.ts
await fs.writeFile(
path.join(fullPath, "src/instrumentation.ts"),
- `import * as Sentry from '@sentry/nextjs';
+ `import { Sentry } from '@mintel/next-observability';
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
- // Server-side initialization
+ await import('./sentry.server.config');
+ }
+
+ if (process.env.NEXT_RUNTIME === 'edge') {
+ await import('./sentry.edge.config');
}
}
@@ -397,8 +402,12 @@ DIRECTUS_DB_PASSWORD=mintel-db-pass
SENTRY_DSN=
# Analytics (Umami)
-NEXT_PUBLIC_UMAMI_WEBSITE_ID=
-NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
+UMAMI_WEBSITE_ID=
+UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
+
+# Notifications (Gotify)
+GOTIFY_URL=
+GOTIFY_TOKEN=
`;
await fs.writeFile(path.join(fullPath, ".env.example"), envExample);
diff --git a/packages/next-observability/package.json b/packages/next-observability/package.json
new file mode 100644
index 0000000..e409054
--- /dev/null
+++ b/packages/next-observability/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@mintel/next-observability",
+ "version": "1.0.0",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://npm.infra.mintel.me"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ },
+ "./client": {
+ "types": "./dist/client.d.ts",
+ "import": "./dist/client.js",
+ "require": "./dist/client.cjs"
+ }
+ },
+ "type": "module",
+ "scripts": {
+ "build": "tsup src/index.ts src/client.ts --format cjs,esm --dts --splitting",
+ "dev": "tsup src/index.ts src/client.ts --format cjs,esm --watch --dts --splitting",
+ "lint": "eslint src/"
+ },
+ "dependencies": {
+ "@mintel/observability": "workspace:*",
+ "@sentry/nextjs": "^8.55.0",
+ "next": "15.1.6"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "devDependencies": {
+ "@mintel/eslint-config": "workspace:*",
+ "@mintel/tsconfig": "workspace:*",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "eslint": "^9.39.2",
+ "tsup": "^8.0.0",
+ "typescript": "^5.0.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+}
diff --git a/packages/next-observability/src/analytics/auto-tracker.tsx b/packages/next-observability/src/analytics/auto-tracker.tsx
new file mode 100644
index 0000000..c2b336d
--- /dev/null
+++ b/packages/next-observability/src/analytics/auto-tracker.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { useEffect } from "react";
+import { usePathname, useSearchParams } from "next/navigation";
+import { useAnalytics } from "./context";
+
+/**
+ * Automatically tracks pageviews on client-side route changes in Next.js.
+ */
+export function AnalyticsAutoTracker() {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const analytics = useAnalytics();
+
+ useEffect(() => {
+ if (!pathname) return;
+
+ const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ""}`;
+ analytics.trackPageview(url);
+ }, [pathname, searchParams, analytics]);
+
+ return null;
+}
diff --git a/packages/next-observability/src/analytics/context.tsx b/packages/next-observability/src/analytics/context.tsx
new file mode 100644
index 0000000..596cf7a
--- /dev/null
+++ b/packages/next-observability/src/analytics/context.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { createContext, useContext, ReactNode, useMemo } from "react";
+import type { AnalyticsService } from "@mintel/observability";
+import {
+ NoopAnalyticsService,
+ UmamiAnalyticsService,
+} from "@mintel/observability";
+
+const AnalyticsContext = createContext(
+ new NoopAnalyticsService(),
+);
+
+export interface AnalyticsContextProviderProps {
+ service?: AnalyticsService;
+ config?: {
+ enabled: boolean;
+ websiteId?: string;
+ apiEndpoint: string;
+ };
+ children: ReactNode;
+}
+
+export function AnalyticsContextProvider({
+ service,
+ config,
+ children,
+}: AnalyticsContextProviderProps) {
+ const activeService = useMemo(() => {
+ if (service) return service;
+ if (config) return new UmamiAnalyticsService(config);
+ return new NoopAnalyticsService();
+ }, [service, config]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAnalytics() {
+ const context = useContext(AnalyticsContext);
+ if (!context) {
+ throw new Error(
+ "useAnalytics must be used within an AnalyticsContextProvider",
+ );
+ }
+ return context;
+}
diff --git a/packages/next-observability/src/client.ts b/packages/next-observability/src/client.ts
new file mode 100644
index 0000000..19e12cc
--- /dev/null
+++ b/packages/next-observability/src/client.ts
@@ -0,0 +1,4 @@
+"use client";
+
+export * from "./analytics/context";
+export * from "./analytics/auto-tracker";
diff --git a/packages/next-observability/src/errors/sentry.ts b/packages/next-observability/src/errors/sentry.ts
new file mode 100644
index 0000000..5a988a8
--- /dev/null
+++ b/packages/next-observability/src/errors/sentry.ts
@@ -0,0 +1,26 @@
+import * as Sentry from "@sentry/nextjs";
+
+export interface SentryConfig {
+ dsn?: string;
+ enabled?: boolean;
+ tracesSampleRate?: number;
+ tunnel?: string;
+ replaysOnErrorSampleRate?: number;
+ replaysSessionSampleRate?: number;
+}
+
+/**
+ * Standardized Sentry initialization for Mintel projects.
+ */
+export function initSentry(config: SentryConfig) {
+ Sentry.init({
+ dsn: config.dsn,
+ enabled: config.enabled ?? Boolean(config.dsn || config.tunnel),
+ tracesSampleRate: config.tracesSampleRate ?? 0,
+ tunnel: config.tunnel,
+ replaysOnErrorSampleRate: config.replaysOnErrorSampleRate ?? 1.0,
+ replaysSessionSampleRate: config.replaysSessionSampleRate ?? 0.1,
+ });
+}
+
+export { Sentry };
diff --git a/packages/next-observability/src/handlers/index.ts b/packages/next-observability/src/handlers/index.ts
new file mode 100644
index 0000000..b682726
--- /dev/null
+++ b/packages/next-observability/src/handlers/index.ts
@@ -0,0 +1,90 @@
+import { NextRequest, NextResponse } from "next/server";
+
+/**
+ * Logic for Umami Smart Proxy Route Handler.
+ */
+export function createUmamiProxyHandler(config: {
+ websiteId?: string;
+ apiEndpoint: string;
+}) {
+ return async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { type, payload } = body;
+
+ if (!config.websiteId) {
+ return NextResponse.json({ status: "ignored" }, { status: 200 });
+ }
+
+ const enhancedPayload = {
+ ...payload,
+ website: config.websiteId,
+ };
+
+ const response = await fetch(`${config.apiEndpoint}/api/send`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "User-Agent": request.headers.get("user-agent") || "Mintel-Proxy",
+ "X-Forwarded-For": request.headers.get("x-forwarded-for") || "",
+ },
+ body: JSON.stringify({ type, payload: enhancedPayload }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return new NextResponse(errorText, { status: response.status });
+ }
+
+ return NextResponse.json({ status: "ok" });
+ } catch (error) {
+ return NextResponse.json(
+ { error: "Internal Server Error" },
+ { status: 500 },
+ );
+ }
+ };
+}
+
+/**
+ * Logic for Sentry/GlitchTip Relay Route Handler.
+ */
+export function createSentryRelayHandler(config: { dsn?: string }) {
+ return async function POST(request: NextRequest) {
+ try {
+ const envelope = await request.text();
+ const lines = envelope.split("\n");
+ if (lines.length < 1) {
+ return NextResponse.json({ error: "Empty envelope" }, { status: 400 });
+ }
+
+ if (!config.dsn) {
+ return NextResponse.json({ status: "ignored" }, { status: 200 });
+ }
+
+ const dsnUrl = new URL(config.dsn);
+ const projectId = dsnUrl.pathname.replace("/", "");
+ const relayUrl = `${dsnUrl.protocol}//${dsnUrl.host}/api/${projectId}/envelope/`;
+
+ const response = await fetch(relayUrl, {
+ method: "POST",
+ body: envelope,
+ headers: {
+ "Content-Type": "application/x-sentry-envelope",
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return new NextResponse(errorText, { status: response.status });
+ }
+
+ return NextResponse.json({ status: "ok" });
+ } catch (error) {
+ return NextResponse.json(
+ { error: "Internal Server Error" },
+ { status: 500 },
+ );
+ }
+ };
+}
diff --git a/packages/next-observability/src/index.ts b/packages/next-observability/src/index.ts
new file mode 100644
index 0000000..ee31d50
--- /dev/null
+++ b/packages/next-observability/src/index.ts
@@ -0,0 +1,2 @@
+export * from "./handlers/index";
+export * from "./errors/sentry";
diff --git a/packages/next-observability/tsconfig.json b/packages/next-observability/tsconfig.json
new file mode 100644
index 0000000..4d48a69
--- /dev/null
+++ b/packages/next-observability/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../tsconfig/base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "jsx": "react-jsx",
+ "esModuleInterop": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/next-utils/src/env.ts b/packages/next-utils/src/env.ts
index af63616..4830251 100644
--- a/packages/next-utils/src/env.ts
+++ b/packages/next-utils/src/env.ts
@@ -1,20 +1,36 @@
-import { z } from 'zod';
+import { z } from "zod";
export const mintelEnvSchema = {
- NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
+ NODE_ENV: z
+ .enum(["development", "production", "test"])
+ .default("development"),
NEXT_PUBLIC_BASE_URL: z.string().url(),
- NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
- NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.string().url().default('https://analytics.infra.mintel.me/script.js'),
+
+ // Analytics (Proxy Pattern)
+ UMAMI_WEBSITE_ID: z.string().optional(),
+ UMAMI_API_ENDPOINT: z
+ .string()
+ .url()
+ .default("https://analytics.infra.mintel.me"),
+
+ // Error Tracking
SENTRY_DSN: z.string().optional(),
- LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
+
+ // Notifications
+ GOTIFY_URL: z.string().url().optional(),
+ GOTIFY_TOKEN: z.string().optional(),
+
+ LOG_LEVEL: z
+ .enum(["trace", "debug", "info", "warn", "error", "fatal"])
+ .default("info"),
MAIL_HOST: z.string().optional(),
MAIL_PORT: z.coerce.number().default(587),
MAIL_USERNAME: z.string().optional(),
MAIL_PASSWORD: z.string().optional(),
MAIL_FROM: z.string().optional(),
MAIL_RECIPIENTS: z.preprocess(
- (val) => (typeof val === 'string' ? val.split(',').filter(Boolean) : val),
- z.array(z.string()).default([])
+ (val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val),
+ z.array(z.string()).default([]),
),
};
@@ -24,11 +40,26 @@ export function validateMintelEnv(schemaExtension = {}) {
...schemaExtension,
});
+ const isBuildTime =
+ process.env.NEXT_PHASE === "phase-production-build" ||
+ process.env.SKIP_ENV_VALIDATION === "true";
+
const result = fullSchema.safeParse(process.env);
if (!result.success) {
- console.error('❌ Invalid environment variables:', result.error.flatten().fieldErrors);
- throw new Error('Invalid environment variables');
+ if (isBuildTime) {
+ console.warn(
+ "⚠️ Some environment variables are missing during build, but skipping strict validation.",
+ );
+ // Return partial data to allow build to continue
+ return process.env as unknown as z.infer;
+ }
+
+ console.error(
+ "❌ Invalid environment variables:",
+ result.error.flatten().fieldErrors,
+ );
+ throw new Error("Invalid environment variables");
}
return result.data;
diff --git a/packages/observability/README.md b/packages/observability/README.md
new file mode 100644
index 0000000..a293c2b
--- /dev/null
+++ b/packages/observability/README.md
@@ -0,0 +1,123 @@
+# @mintel/observability
+
+Standardized observability package for the Mintel ecosystem, providing Umami analytics and Sentry/GlitchTip error tracking with a focus on privacy and ad-blocker resilience.
+
+## Features
+
+- **Umami Smart Proxy**: Track analytics without external scripts and hide your Website ID.
+- **Sentry Relay**: Bypass ad-blockers for error tracking by relaying envelopes through your own server.
+- **Unified API**: consistent interface for tracking across multiple projects.
+
+## Installation
+
+```bash
+pnpm add @mintel/observability @sentry/nextjs
+```
+
+## Usage
+
+### 1. Unified Environment (via @mintel/next-utils)
+
+Define the following environment variables:
+
+```bash
+# Analytics
+UMAMI_WEBSITE_ID=your-website-id
+UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
+
+# Error Tracking
+SENTRY_DSN=your-sentry-dsn
+```
+
+Note: No `NEXT_PUBLIC_` prefix is required for these anymore, as they are handled by server-side proxies.
+
+### 2. Analytics Setup
+
+In your root layout:
+
+```tsx
+import {
+ AnalyticsContextProvider,
+ AnalyticsAutoTracker,
+ UmamiAnalyticsService,
+} from "@mintel/observability";
+
+const analytics = new UmamiAnalyticsService({
+ enabled: true,
+ websiteId: process.env.UMAMI_WEBSITE_ID, // Server-side
+ apiEndpoint:
+ typeof window === "undefined" ? process.env.UMAMI_API_ENDPOINT : "/stats",
+});
+
+export default function Layout({ children }) {
+ return (
+
+
+ {children}
+
+ );
+}
+```
+
+### 3. Route Handlers
+
+Create a proxy for Umami:
+`app/stats/api/send/route.ts`
+
+```ts
+import { createUmamiProxyHandler } from "@mintel/observability";
+export const POST = await createUmamiProxyHandler({
+ websiteId: process.env.UMAMI_WEBSITE_ID,
+ apiEndpoint: process.env.UMAMI_API_ENDPOINT,
+});
+```
+
+Create a relay for Sentry:
+`app/errors/api/relay/route.ts`
+
+```ts
+import { createSentryRelayHandler } from "@mintel/observability";
+export const POST = await createSentryRelayHandler({
+ dsn: process.env.SENTRY_DSN,
+});
+```
+
+### 4. Notification Setup (Server-side)
+
+```ts
+import { GotifyNotificationService } from "@mintel/observability";
+
+const notifications = new GotifyNotificationService({
+ enabled: true,
+ url: process.env.GOTIFY_URL,
+ token: process.env.GOTIFY_TOKEN,
+});
+
+await notifications.notify({
+ title: "Lead Capture",
+ message: "New contact form submission",
+ priority: 5,
+});
+```
+
+### 5. Sentry Configuration
+
+Use `initSentry` in your `sentry.server.config.ts` and `sentry.client.config.ts`.
+
+On the client, use the tunnel:
+
+```ts
+initSentry({
+ dsn: "https://public@errors.infra.mintel.me/1", // Placeholder
+ tunnel: "/errors/api/relay",
+});
+```
+
+## Architecture
+
+This package implements the **Smart Proxy** pattern:
+
+- The client NEVER knows the real `UMAMI_WEBSITE_ID`.
+- Tracking events are sent to your own domain (`/stats/api/send`).
+- Your server injects the secret ID and forwards to Umami.
+- This bypasses ad-blockers and keeps your configuration secure.
diff --git a/packages/observability/package.json b/packages/observability/package.json
new file mode 100644
index 0000000..2a65733
--- /dev/null
+++ b/packages/observability/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@mintel/observability",
+ "version": "1.0.0",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://npm.infra.mintel.me"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ }
+ },
+ "type": "module",
+ "scripts": {
+ "build": "tsup src/index.ts --format cjs,esm --dts",
+ "dev": "tsup src/index.ts --format cjs,esm --watch --dts",
+ "lint": "eslint src/",
+ "test": "vitest run"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "@mintel/eslint-config": "workspace:*",
+ "@mintel/tsconfig": "workspace:*",
+ "eslint": "^9.39.2",
+ "tsup": "^8.0.0",
+ "typescript": "^5.0.0",
+ "vitest": "^2.0.0"
+ }
+}
diff --git a/packages/observability/src/analytics/noop.test.ts b/packages/observability/src/analytics/noop.test.ts
new file mode 100644
index 0000000..f2feedb
--- /dev/null
+++ b/packages/observability/src/analytics/noop.test.ts
@@ -0,0 +1,14 @@
+import { describe, it, expect } from "vitest";
+import { NoopAnalyticsService } from "./noop";
+
+describe("NoopAnalyticsService", () => {
+ it("should not throw on track", () => {
+ const service = new NoopAnalyticsService();
+ expect(() => service.track("test")).not.toThrow();
+ });
+
+ it("should not throw on trackPageview", () => {
+ const service = new NoopAnalyticsService();
+ expect(() => service.trackPageview()).not.toThrow();
+ });
+});
diff --git a/packages/observability/src/analytics/noop.ts b/packages/observability/src/analytics/noop.ts
new file mode 100644
index 0000000..f2e77c0
--- /dev/null
+++ b/packages/observability/src/analytics/noop.ts
@@ -0,0 +1,15 @@
+import type { AnalyticsService, AnalyticsEventProperties } from "./service";
+
+/**
+ * No-operation analytics service.
+ * Used when analytics are disabled or for local development.
+ */
+export class NoopAnalyticsService implements AnalyticsService {
+ track(eventName: string, props?: AnalyticsEventProperties): void {
+ // Do nothing
+ }
+
+ trackPageview(url?: string): void {
+ // Do nothing
+ }
+}
diff --git a/packages/observability/src/analytics/service.ts b/packages/observability/src/analytics/service.ts
new file mode 100644
index 0000000..6e9dc5d
--- /dev/null
+++ b/packages/observability/src/analytics/service.ts
@@ -0,0 +1,31 @@
+/**
+ * Type definition for analytics event properties.
+ */
+export type AnalyticsEventProperties = Record<
+ string,
+ string | number | boolean | null | undefined
+>;
+
+/**
+ * Interface for analytics service implementations.
+ *
+ * This interface defines the contract for all analytics services,
+ * allowing for different implementations (Umami, Google Analytics, etc.)
+ * while maintaining a consistent API.
+ */
+export interface AnalyticsService {
+ /**
+ * Track a custom event with optional properties.
+ *
+ * @param eventName - The name of the event to track
+ * @param props - Optional event properties (metadata)
+ */
+ track(eventName: string, props?: AnalyticsEventProperties): void;
+
+ /**
+ * Track a pageview.
+ *
+ * @param url - The URL to track (defaults to current location)
+ */
+ trackPageview(url?: string): void;
+}
diff --git a/packages/observability/src/analytics/umami.test.ts b/packages/observability/src/analytics/umami.test.ts
new file mode 100644
index 0000000..e1ab057
--- /dev/null
+++ b/packages/observability/src/analytics/umami.test.ts
@@ -0,0 +1,74 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { UmamiAnalyticsService } from "./umami";
+
+describe("UmamiAnalyticsService", () => {
+ const mockConfig = {
+ websiteId: "test-website-id",
+ apiEndpoint: "https://analytics.test",
+ enabled: true,
+ };
+
+ const mockLogger = {
+ debug: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ trace: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ global.fetch = vi.fn();
+ });
+
+ it("should not send payload if disabled", async () => {
+ const service = new UmamiAnalyticsService({
+ ...mockConfig,
+ enabled: false,
+ });
+ service.track("test-event");
+ expect(global.fetch).not.toHaveBeenCalled();
+ });
+
+ it("should send payload with correct data for track", async () => {
+ const service = new UmamiAnalyticsService(mockConfig, mockLogger);
+
+ (global.fetch as any).mockResolvedValue({ ok: true });
+
+ service.track("test-event", { foo: "bar" });
+
+ // Wait for async sendPayload
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ "https://analytics.test/api/send",
+ expect.objectContaining({
+ method: "POST",
+ body: expect.stringContaining('"type":"event"'),
+ }),
+ );
+
+ const callBody = JSON.parse((global.fetch as any).mock.calls[0][1].body);
+ expect(callBody.payload.name).toBe("test-event");
+ expect(callBody.payload.data.foo).toBe("bar");
+ expect(callBody.payload.website).toBe("test-website-id");
+ });
+
+ it("should log warning if send fails", async () => {
+ const service = new UmamiAnalyticsService(mockConfig, mockLogger);
+
+ (global.fetch as any).mockResolvedValue({
+ ok: false,
+ status: 500,
+ text: () => Promise.resolve("Internal error"),
+ });
+
+ service.track("test-event");
+
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ expect(mockLogger.warn).toHaveBeenCalledWith(
+ "Umami API responded with error",
+ expect.objectContaining({ status: 500 }),
+ );
+ });
+});
diff --git a/packages/observability/src/analytics/umami.ts b/packages/observability/src/analytics/umami.ts
new file mode 100644
index 0000000..f140fea
--- /dev/null
+++ b/packages/observability/src/analytics/umami.ts
@@ -0,0 +1,115 @@
+import type { AnalyticsService, AnalyticsEventProperties } from "./service";
+
+export interface UmamiConfig {
+ websiteId?: string;
+ apiEndpoint: string; // The endpoint to send to (proxied or direct)
+ enabled: boolean;
+}
+
+export interface Logger {
+ debug(msg: string, data?: any): void;
+ warn(msg: string, data?: any): void;
+ error(msg: string, data?: any): void;
+ trace(msg: string, data?: any): void;
+}
+
+/**
+ * Umami Analytics Service Implementation (Script-less/Proxy edition).
+ */
+export class UmamiAnalyticsService implements AnalyticsService {
+ private logger?: Logger;
+
+ constructor(
+ private config: UmamiConfig,
+ logger?: Logger,
+ ) {
+ this.logger = logger;
+ }
+
+ private async sendPayload(type: "event", data: Record) {
+ if (!this.config.enabled) return;
+
+ const isClient = typeof window !== "undefined";
+ const websiteId = this.config.websiteId;
+
+ if (!isClient && !websiteId) {
+ this.logger?.warn(
+ "Umami tracking called on server but no Website ID configured",
+ );
+ return;
+ }
+
+ try {
+ const payload = {
+ website: websiteId,
+ hostname: isClient ? window.location.hostname : "server",
+ screen: isClient
+ ? `${window.screen.width}x${window.screen.height}`
+ : undefined,
+ language: isClient ? navigator.language : undefined,
+ referrer: isClient ? document.referrer : undefined,
+ ...data,
+ };
+
+ this.logger?.trace("Sending analytics payload", { type, url: data.url });
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
+
+ try {
+ const response = await fetch(`${this.config.apiEndpoint}/api/send`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "User-Agent": isClient ? navigator.userAgent : "Mintel-Server",
+ },
+ body: JSON.stringify({ type, payload }),
+ keepalive: true,
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ this.logger?.warn("Umami API responded with error", {
+ status: response.status,
+ error: errorText.slice(0, 100),
+ });
+ }
+ } catch (fetchError) {
+ clearTimeout(timeoutId);
+ if ((fetchError as Error).name === "AbortError") {
+ this.logger?.error("Umami request timed out");
+ } else {
+ throw fetchError;
+ }
+ }
+ } catch (error) {
+ this.logger?.error("Failed to send analytics", {
+ error: (error as Error).message,
+ });
+ }
+ }
+
+ track(eventName: string, props?: AnalyticsEventProperties) {
+ this.sendPayload("event", {
+ name: eventName,
+ data: props,
+ url:
+ typeof window !== "undefined"
+ ? window.location.pathname + window.location.search
+ : undefined,
+ });
+ }
+
+ trackPageview(url?: string) {
+ this.sendPayload("event", {
+ url:
+ url ||
+ (typeof window !== "undefined"
+ ? window.location.pathname + window.location.search
+ : undefined),
+ });
+ }
+}
diff --git a/packages/observability/src/index.ts b/packages/observability/src/index.ts
new file mode 100644
index 0000000..cd62df6
--- /dev/null
+++ b/packages/observability/src/index.ts
@@ -0,0 +1,9 @@
+// Analytics
+export * from "./analytics/service";
+export * from "./analytics/umami";
+export * from "./analytics/noop";
+
+// Notifications
+export * from "./notifications/service";
+export * from "./notifications/gotify";
+export * from "./notifications/noop";
diff --git a/packages/observability/src/notifications/gotify.test.ts b/packages/observability/src/notifications/gotify.test.ts
new file mode 100644
index 0000000..d869a3d
--- /dev/null
+++ b/packages/observability/src/notifications/gotify.test.ts
@@ -0,0 +1,82 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { GotifyNotificationService } from "./gotify";
+
+describe("GotifyNotificationService", () => {
+ const mockConfig = {
+ url: "https://gotify.test",
+ token: "test-token",
+ enabled: true,
+ };
+
+ const mockLogger = {
+ error: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ global.fetch = vi.fn();
+ });
+
+ it("should not notify if disabled", async () => {
+ const service = new GotifyNotificationService({
+ ...mockConfig,
+ enabled: false,
+ });
+ await service.notify({ title: "test", message: "test" });
+ expect(global.fetch).not.toHaveBeenCalled();
+ });
+
+ it("should send correct payload to Gotify", async () => {
+ const service = new GotifyNotificationService(mockConfig, mockLogger);
+ (global.fetch as any).mockResolvedValue({ ok: true });
+
+ await service.notify({
+ title: "Alert",
+ message: "Critical issue",
+ priority: 8,
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ "https://gotify.test/message?token=test-token",
+ expect.objectContaining({
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ title: "Alert",
+ message: "Critical issue",
+ priority: 8,
+ }),
+ }),
+ );
+ });
+
+ it("should handle missing trailing slash in URL", async () => {
+ const service = new GotifyNotificationService({
+ ...mockConfig,
+ url: "https://gotify.test",
+ });
+ (global.fetch as any).mockResolvedValue({ ok: true });
+
+ await service.notify({ title: "test", message: "test" });
+
+ expect((global.fetch as any).mock.calls[0][0]).toBe(
+ "https://gotify.test/message?token=test-token",
+ );
+ });
+
+ it("should log error if notify fails", async () => {
+ const service = new GotifyNotificationService(mockConfig, mockLogger);
+ (global.fetch as any).mockResolvedValue({
+ ok: false,
+ status: 401,
+ text: () => Promise.resolve("Unauthorized"),
+ });
+
+ await service.notify({ title: "test", message: "test" });
+
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ "Gotify notification failed",
+ expect.objectContaining({ status: 401 }),
+ );
+ });
+});
diff --git a/packages/observability/src/notifications/gotify.ts b/packages/observability/src/notifications/gotify.ts
new file mode 100644
index 0000000..387b16d
--- /dev/null
+++ b/packages/observability/src/notifications/gotify.ts
@@ -0,0 +1,56 @@
+import { NotificationOptions, NotificationService } from "./service";
+
+export interface GotifyConfig {
+ url: string;
+ token: string;
+ enabled: boolean;
+}
+
+/**
+ * Gotify Notification Service implementation.
+ */
+export class GotifyNotificationService implements NotificationService {
+ constructor(
+ private config: GotifyConfig,
+ private logger?: { error(msg: string, data?: any): void },
+ ) {}
+
+ async notify(options: NotificationOptions): Promise {
+ if (!this.config.enabled) return;
+
+ try {
+ const { title, message, priority = 4 } = options;
+
+ // Ensure we have a trailing slash for base URL, then append 'message'
+ const baseUrl = this.config.url.endsWith("/")
+ ? this.config.url
+ : `${this.config.url}/`;
+ const url = new URL("message", baseUrl);
+ url.searchParams.set("token", this.config.token);
+
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ title,
+ message,
+ priority,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ this.logger?.error("Gotify notification failed", {
+ status: response.status,
+ error: errorText.slice(0, 100),
+ });
+ }
+ } catch (error) {
+ this.logger?.error("Gotify notification error", {
+ error: (error as Error).message,
+ });
+ }
+ }
+}
diff --git a/packages/observability/src/notifications/noop.test.ts b/packages/observability/src/notifications/noop.test.ts
new file mode 100644
index 0000000..486c625
--- /dev/null
+++ b/packages/observability/src/notifications/noop.test.ts
@@ -0,0 +1,11 @@
+import { describe, it, expect } from "vitest";
+import { NoopNotificationService } from "./noop";
+
+describe("NoopNotificationService", () => {
+ it("should not throw on notify", async () => {
+ const service = new NoopNotificationService();
+ await expect(
+ service.notify({ title: "test", message: "test" }),
+ ).resolves.not.toThrow();
+ });
+});
diff --git a/packages/observability/src/notifications/noop.ts b/packages/observability/src/notifications/noop.ts
new file mode 100644
index 0000000..8040f56
--- /dev/null
+++ b/packages/observability/src/notifications/noop.ts
@@ -0,0 +1,10 @@
+import { NotificationService } from "./service";
+
+/**
+ * No-operation notification service.
+ */
+export class NoopNotificationService implements NotificationService {
+ async notify(): Promise {
+ // Do nothing
+ }
+}
diff --git a/packages/observability/src/notifications/service.ts b/packages/observability/src/notifications/service.ts
new file mode 100644
index 0000000..2c3a883
--- /dev/null
+++ b/packages/observability/src/notifications/service.ts
@@ -0,0 +1,16 @@
+export interface NotificationOptions {
+ title: string;
+ message: string;
+ priority?: number;
+}
+
+/**
+ * Interface for notification service implementations.
+ * Allows for different implementations (Gotify, Slack, Email, etc.)
+ */
+export interface NotificationService {
+ /**
+ * Send a notification.
+ */
+ notify(options: NotificationOptions): Promise;
+}
diff --git a/packages/observability/tsconfig.json b/packages/observability/tsconfig.json
new file mode 100644
index 0000000..d55c842
--- /dev/null
+++ b/packages/observability/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig/base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "jsx": "react-jsx",
+ "esModuleInterop": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62bb93c..c0bdc66 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -80,9 +80,18 @@ importers:
apps/sample-website:
dependencies:
+ '@mintel/next-observability':
+ specifier: workspace:*
+ version: link:../../packages/next-observability
'@mintel/next-utils':
specifier: workspace:*
version: link:../../packages/next-utils
+ '@mintel/observability':
+ specifier: workspace:*
+ version: link:../../packages/observability
+ '@sentry/nextjs':
+ specifier: ^8.55.0
+ version: 8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.104.1)
next:
specifier: 15.1.6
version: 15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -289,6 +298,46 @@ importers:
specifier: ^4.8.2
version: 4.8.2(next@15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
+ packages/next-observability:
+ dependencies:
+ '@mintel/observability':
+ specifier: workspace:*
+ version: link:../observability
+ '@sentry/nextjs':
+ specifier: ^8.55.0
+ version: 8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.104.1(@swc/core@1.15.11))
+ next:
+ specifier: 15.1.6
+ version: 15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ devDependencies:
+ '@mintel/eslint-config':
+ specifier: workspace:*
+ version: link:../eslint-config
+ '@mintel/tsconfig':
+ specifier: workspace:*
+ version: link:../tsconfig
+ '@types/react':
+ specifier: ^19.0.0
+ version: 19.2.10
+ '@types/react-dom':
+ specifier: ^19.0.0
+ version: 19.2.3(@types/react@19.2.10)
+ eslint:
+ specifier: ^9.39.2
+ version: 9.39.2(jiti@2.6.1)
+ react:
+ specifier: ^19.0.0
+ version: 19.2.4
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.2.4(react@19.2.4)
+ tsup:
+ specifier: ^8.0.0
+ version: 8.5.1(@swc/core@1.15.11)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+
packages/next-utils:
dependencies:
'@directus/sdk':
@@ -320,6 +369,27 @@ importers:
specifier: ^5.0.0
version: 5.9.3
+ packages/observability:
+ devDependencies:
+ '@mintel/eslint-config':
+ specifier: workspace:*
+ version: link:../eslint-config
+ '@mintel/tsconfig':
+ specifier: workspace:*
+ version: link:../tsconfig
+ eslint:
+ specifier: ^9.39.2
+ version: 9.39.2(jiti@2.6.1)
+ tsup:
+ specifier: ^8.0.0
+ version: 8.5.1(@swc/core@1.15.11)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+ vitest:
+ specifier: ^2.0.0
+ version: 2.1.9(@types/node@20.19.30)(happy-dom@20.4.0)(jsdom@27.4.0)(terser@5.46.0)
+
packages/tsconfig: {}
packages:
@@ -598,102 +668,204 @@ packages:
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
+ '@esbuild/aix-ppc64@0.21.5':
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+
'@esbuild/aix-ppc64@0.27.2':
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
+ '@esbuild/android-arm64@0.21.5':
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
'@esbuild/android-arm64@0.27.2':
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
+ '@esbuild/android-arm@0.21.5':
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
'@esbuild/android-arm@0.27.2':
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
+ '@esbuild/android-x64@0.21.5':
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
'@esbuild/android-x64@0.27.2':
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
+ '@esbuild/darwin-arm64@0.21.5':
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
'@esbuild/darwin-arm64@0.27.2':
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
+ '@esbuild/darwin-x64@0.21.5':
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
'@esbuild/darwin-x64@0.27.2':
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
+ '@esbuild/freebsd-arm64@0.21.5':
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
'@esbuild/freebsd-arm64@0.27.2':
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
+ '@esbuild/freebsd-x64@0.21.5':
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
'@esbuild/freebsd-x64@0.27.2':
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
+ '@esbuild/linux-arm64@0.21.5':
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
'@esbuild/linux-arm64@0.27.2':
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
+ '@esbuild/linux-arm@0.21.5':
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
'@esbuild/linux-arm@0.27.2':
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
+ '@esbuild/linux-ia32@0.21.5':
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
'@esbuild/linux-ia32@0.27.2':
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
+ '@esbuild/linux-loong64@0.21.5':
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
'@esbuild/linux-loong64@0.27.2':
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
+ '@esbuild/linux-mips64el@0.21.5':
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
'@esbuild/linux-mips64el@0.27.2':
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
+ '@esbuild/linux-ppc64@0.21.5':
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
'@esbuild/linux-ppc64@0.27.2':
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
+ '@esbuild/linux-riscv64@0.21.5':
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
'@esbuild/linux-riscv64@0.27.2':
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
+ '@esbuild/linux-s390x@0.21.5':
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
'@esbuild/linux-s390x@0.27.2':
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
+ '@esbuild/linux-x64@0.21.5':
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
'@esbuild/linux-x64@0.27.2':
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
engines: {node: '>=18'}
@@ -706,6 +878,12 @@ packages:
cpu: [arm64]
os: [netbsd]
+ '@esbuild/netbsd-x64@0.21.5':
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
'@esbuild/netbsd-x64@0.27.2':
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
engines: {node: '>=18'}
@@ -718,6 +896,12 @@ packages:
cpu: [arm64]
os: [openbsd]
+ '@esbuild/openbsd-x64@0.21.5':
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
'@esbuild/openbsd-x64@0.27.2':
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
engines: {node: '>=18'}
@@ -730,24 +914,48 @@ packages:
cpu: [arm64]
os: [openharmony]
+ '@esbuild/sunos-x64@0.21.5':
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
'@esbuild/sunos-x64@0.27.2':
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
+ '@esbuild/win32-arm64@0.21.5':
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
'@esbuild/win32-arm64@0.27.2':
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
+ '@esbuild/win32-ia32@0.21.5':
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
'@esbuild/win32-ia32@0.27.2':
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
+ '@esbuild/win32-x64@0.21.5':
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
'@esbuild/win32-x64@0.27.2':
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
engines: {node: '>=18'}
@@ -2109,12 +2317,26 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+ '@vitest/expect@2.1.9':
+ resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
+
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/expect@4.0.18':
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
+ '@vitest/mocker@2.1.9':
+ resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
'@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies:
@@ -2137,30 +2359,45 @@ packages:
vite:
optional: true
+ '@vitest/pretty-format@2.1.9':
+ resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
+
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/pretty-format@4.0.18':
resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
+ '@vitest/runner@2.1.9':
+ resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
+
'@vitest/runner@3.2.4':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
'@vitest/runner@4.0.18':
resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
+ '@vitest/snapshot@2.1.9':
+ resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
+
'@vitest/snapshot@3.2.4':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/snapshot@4.0.18':
resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
+ '@vitest/spy@2.1.9':
+ resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
+
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/spy@4.0.18':
resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
+ '@vitest/utils@2.1.9':
+ resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
+
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
@@ -2824,6 +3061,11 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ esbuild@0.21.5:
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+ engines: {node: '>=12'}
+ hasBin: true
+
esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
engines: {node: '>=18'}
@@ -3906,6 +4148,9 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
+ pathe@1.1.2:
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -4510,6 +4755,10 @@ packages:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
+ tinyrainbow@1.2.0:
+ resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+ engines: {node: '>=14.0.0'}
+
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
@@ -4518,6 +4767,10 @@ packages:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
+ tinyspy@3.0.2:
+ resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+ engines: {node: '>=14.0.0'}
+
tinyspy@4.0.4:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
@@ -4668,11 +4921,47 @@ packages:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
+ vite-node@2.1.9:
+ resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
+ vite@5.4.21:
+ resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+
vite@7.3.1:
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -4713,6 +5002,31 @@ packages:
yaml:
optional: true
+ vitest@2.1.9:
+ resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/node': ^18.0.0 || >=20.0.0
+ '@vitest/browser': 2.1.9
+ '@vitest/ui': 2.1.9
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -5349,81 +5663,150 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@esbuild/aix-ppc64@0.21.5':
+ optional: true
+
'@esbuild/aix-ppc64@0.27.2':
optional: true
+ '@esbuild/android-arm64@0.21.5':
+ optional: true
+
'@esbuild/android-arm64@0.27.2':
optional: true
+ '@esbuild/android-arm@0.21.5':
+ optional: true
+
'@esbuild/android-arm@0.27.2':
optional: true
+ '@esbuild/android-x64@0.21.5':
+ optional: true
+
'@esbuild/android-x64@0.27.2':
optional: true
+ '@esbuild/darwin-arm64@0.21.5':
+ optional: true
+
'@esbuild/darwin-arm64@0.27.2':
optional: true
+ '@esbuild/darwin-x64@0.21.5':
+ optional: true
+
'@esbuild/darwin-x64@0.27.2':
optional: true
+ '@esbuild/freebsd-arm64@0.21.5':
+ optional: true
+
'@esbuild/freebsd-arm64@0.27.2':
optional: true
+ '@esbuild/freebsd-x64@0.21.5':
+ optional: true
+
'@esbuild/freebsd-x64@0.27.2':
optional: true
+ '@esbuild/linux-arm64@0.21.5':
+ optional: true
+
'@esbuild/linux-arm64@0.27.2':
optional: true
+ '@esbuild/linux-arm@0.21.5':
+ optional: true
+
'@esbuild/linux-arm@0.27.2':
optional: true
+ '@esbuild/linux-ia32@0.21.5':
+ optional: true
+
'@esbuild/linux-ia32@0.27.2':
optional: true
+ '@esbuild/linux-loong64@0.21.5':
+ optional: true
+
'@esbuild/linux-loong64@0.27.2':
optional: true
+ '@esbuild/linux-mips64el@0.21.5':
+ optional: true
+
'@esbuild/linux-mips64el@0.27.2':
optional: true
+ '@esbuild/linux-ppc64@0.21.5':
+ optional: true
+
'@esbuild/linux-ppc64@0.27.2':
optional: true
+ '@esbuild/linux-riscv64@0.21.5':
+ optional: true
+
'@esbuild/linux-riscv64@0.27.2':
optional: true
+ '@esbuild/linux-s390x@0.21.5':
+ optional: true
+
'@esbuild/linux-s390x@0.27.2':
optional: true
+ '@esbuild/linux-x64@0.21.5':
+ optional: true
+
'@esbuild/linux-x64@0.27.2':
optional: true
'@esbuild/netbsd-arm64@0.27.2':
optional: true
+ '@esbuild/netbsd-x64@0.21.5':
+ optional: true
+
'@esbuild/netbsd-x64@0.27.2':
optional: true
'@esbuild/openbsd-arm64@0.27.2':
optional: true
+ '@esbuild/openbsd-x64@0.21.5':
+ optional: true
+
'@esbuild/openbsd-x64@0.27.2':
optional: true
'@esbuild/openharmony-arm64@0.27.2':
optional: true
+ '@esbuild/sunos-x64@0.21.5':
+ optional: true
+
'@esbuild/sunos-x64@0.27.2':
optional: true
+ '@esbuild/win32-arm64@0.21.5':
+ optional: true
+
'@esbuild/win32-arm64@0.27.2':
optional: true
+ '@esbuild/win32-ia32@0.21.5':
+ optional: true
+
'@esbuild/win32-ia32@0.27.2':
optional: true
+ '@esbuild/win32-x64@0.21.5':
+ optional: true
+
'@esbuild/win32-x64@0.27.2':
optional: true
@@ -6355,6 +6738,33 @@ snapshots:
'@sentry/core@8.55.0': {}
+ '@sentry/nextjs@8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.104.1(@swc/core@1.15.11))':
+ dependencies:
+ '@opentelemetry/api': 1.9.0
+ '@opentelemetry/semantic-conventions': 1.39.0
+ '@rollup/plugin-commonjs': 28.0.1(rollup@3.29.5)
+ '@sentry-internal/browser-utils': 8.55.0
+ '@sentry/core': 8.55.0
+ '@sentry/node': 8.55.0
+ '@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0)
+ '@sentry/react': 8.55.0(react@19.2.4)
+ '@sentry/vercel-edge': 8.55.0
+ '@sentry/webpack-plugin': 2.22.7(webpack@5.104.1(@swc/core@1.15.11))
+ chalk: 3.0.0
+ next: 15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ resolve: 1.22.8
+ rollup: 3.29.5
+ stacktrace-parser: 0.1.11
+ transitivePeerDependencies:
+ - '@opentelemetry/context-async-hooks'
+ - '@opentelemetry/core'
+ - '@opentelemetry/instrumentation'
+ - '@opentelemetry/sdk-trace-base'
+ - encoding
+ - react
+ - supports-color
+ - webpack
+
'@sentry/nextjs@8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.104.1)':
dependencies:
'@opentelemetry/api': 1.9.0
@@ -6444,6 +6854,16 @@ snapshots:
'@opentelemetry/api': 1.9.0
'@sentry/core': 8.55.0
+ '@sentry/webpack-plugin@2.22.7(webpack@5.104.1(@swc/core@1.15.11))':
+ dependencies:
+ '@sentry/bundler-plugin-core': 2.22.7
+ unplugin: 1.0.1
+ uuid: 9.0.1
+ webpack: 5.104.1(@swc/core@1.15.11)
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
'@sentry/webpack-plugin@2.22.7(webpack@5.104.1)':
dependencies:
'@sentry/bundler-plugin-core': 2.22.7
@@ -6818,6 +7238,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vitest/expect@2.1.9':
+ dependencies:
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.3.3
+ tinyrainbow: 1.2.0
+
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.3
@@ -6835,6 +7262,14 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.0.3
+ '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.30)(terser@5.46.0))':
+ dependencies:
+ '@vitest/spy': 2.1.9
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 5.4.21(@types/node@20.19.30)(terser@5.46.0)
+
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@20.19.30)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 3.2.4
@@ -6851,6 +7286,10 @@ snapshots:
optionalDependencies:
vite: 7.3.1(@types/node@20.19.30)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
+ '@vitest/pretty-format@2.1.9':
+ dependencies:
+ tinyrainbow: 1.2.0
+
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
@@ -6859,6 +7298,11 @@ snapshots:
dependencies:
tinyrainbow: 3.0.3
+ '@vitest/runner@2.1.9':
+ dependencies:
+ '@vitest/utils': 2.1.9
+ pathe: 1.1.2
+
'@vitest/runner@3.2.4':
dependencies:
'@vitest/utils': 3.2.4
@@ -6870,6 +7314,12 @@ snapshots:
'@vitest/utils': 4.0.18
pathe: 2.0.3
+ '@vitest/snapshot@2.1.9':
+ dependencies:
+ '@vitest/pretty-format': 2.1.9
+ magic-string: 0.30.21
+ pathe: 1.1.2
+
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
@@ -6882,12 +7332,22 @@ snapshots:
magic-string: 0.30.21
pathe: 2.0.3
+ '@vitest/spy@2.1.9':
+ dependencies:
+ tinyspy: 3.0.2
+
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.4
'@vitest/spy@4.0.18': {}
+ '@vitest/utils@2.1.9':
+ dependencies:
+ '@vitest/pretty-format': 2.1.9
+ loupe: 3.2.1
+ tinyrainbow: 1.2.0
+
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
@@ -7632,6 +8092,32 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ esbuild@0.21.5:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.21.5
+ '@esbuild/android-arm': 0.21.5
+ '@esbuild/android-arm64': 0.21.5
+ '@esbuild/android-x64': 0.21.5
+ '@esbuild/darwin-arm64': 0.21.5
+ '@esbuild/darwin-x64': 0.21.5
+ '@esbuild/freebsd-arm64': 0.21.5
+ '@esbuild/freebsd-x64': 0.21.5
+ '@esbuild/linux-arm': 0.21.5
+ '@esbuild/linux-arm64': 0.21.5
+ '@esbuild/linux-ia32': 0.21.5
+ '@esbuild/linux-loong64': 0.21.5
+ '@esbuild/linux-mips64el': 0.21.5
+ '@esbuild/linux-ppc64': 0.21.5
+ '@esbuild/linux-riscv64': 0.21.5
+ '@esbuild/linux-s390x': 0.21.5
+ '@esbuild/linux-x64': 0.21.5
+ '@esbuild/netbsd-x64': 0.21.5
+ '@esbuild/openbsd-x64': 0.21.5
+ '@esbuild/sunos-x64': 0.21.5
+ '@esbuild/win32-arm64': 0.21.5
+ '@esbuild/win32-ia32': 0.21.5
+ '@esbuild/win32-x64': 0.21.5
+
esbuild@0.27.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2
@@ -8832,6 +9318,8 @@ snapshots:
path-type@4.0.0: {}
+ pathe@1.1.2: {}
+
pathe@2.0.3: {}
pathval@2.0.1: {}
@@ -9467,6 +9955,17 @@ snapshots:
term-size@2.2.1: {}
+ terser-webpack-plugin@5.3.16(@swc/core@1.15.11)(webpack@5.104.1(@swc/core@1.15.11)):
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ jest-worker: 27.5.1
+ schema-utils: 4.3.3
+ serialize-javascript: 6.0.2
+ terser: 5.46.0
+ webpack: 5.104.1(@swc/core@1.15.11)
+ optionalDependencies:
+ '@swc/core': 1.15.11
+
terser-webpack-plugin@5.3.16(webpack@5.104.1):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -9504,10 +10003,14 @@ snapshots:
tinypool@1.1.1: {}
+ tinyrainbow@1.2.0: {}
+
tinyrainbow@2.0.0: {}
tinyrainbow@3.0.3: {}
+ tinyspy@3.0.2: {}
+
tinyspy@4.0.4: {}
tldts-core@7.0.21: {}
@@ -9703,6 +10206,24 @@ snapshots:
uuid@9.0.1: {}
+ vite-node@2.1.9(@types/node@20.19.30)(terser@5.46.0):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 1.1.2
+ vite: 5.4.21(@types/node@20.19.30)(terser@5.46.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vite-node@3.2.4(@types/node@20.19.30)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
cac: 6.7.14
@@ -9724,6 +10245,16 @@ snapshots:
- tsx
- yaml
+ vite@5.4.21(@types/node@20.19.30)(terser@5.46.0):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.5.6
+ rollup: 4.57.1
+ optionalDependencies:
+ '@types/node': 20.19.30
+ fsevents: 2.3.3
+ terser: 5.46.0
+
vite@7.3.1(@types/node@20.19.30)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
esbuild: 0.27.2
@@ -9740,6 +10271,43 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.2
+ vitest@2.1.9(@types/node@20.19.30)(happy-dom@20.4.0)(jsdom@27.4.0)(terser@5.46.0):
+ dependencies:
+ '@vitest/expect': 2.1.9
+ '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.30)(terser@5.46.0))
+ '@vitest/pretty-format': 2.1.9
+ '@vitest/runner': 2.1.9
+ '@vitest/snapshot': 2.1.9
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.3.3
+ debug: 4.4.3
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ pathe: 1.1.2
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinypool: 1.1.1
+ tinyrainbow: 1.2.0
+ vite: 5.4.21(@types/node@20.19.30)(terser@5.46.0)
+ vite-node: 2.1.9(@types/node@20.19.30)(terser@5.46.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 20.19.30
+ happy-dom: 20.4.0
+ jsdom: 27.4.0
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vitest@3.2.4(@types/node@20.19.30)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@types/chai': 5.2.3
@@ -9872,6 +10440,38 @@ snapshots:
- esbuild
- uglify-js
+ webpack@5.104.1(@swc/core@1.15.11):
+ dependencies:
+ '@types/eslint-scope': 3.7.7
+ '@types/estree': 1.0.8
+ '@types/json-schema': 7.0.15
+ '@webassemblyjs/ast': 1.14.1
+ '@webassemblyjs/wasm-edit': 1.14.1
+ '@webassemblyjs/wasm-parser': 1.14.1
+ acorn: 8.15.0
+ acorn-import-phases: 1.0.4(acorn@8.15.0)
+ browserslist: 4.28.1
+ chrome-trace-event: 1.0.4
+ enhanced-resolve: 5.18.4
+ es-module-lexer: 2.0.0
+ eslint-scope: 5.1.1
+ events: 3.3.0
+ glob-to-regexp: 0.4.1
+ graceful-fs: 4.2.11
+ json-parse-even-better-errors: 2.3.1
+ loader-runner: 4.3.1
+ mime-types: 2.1.35
+ neo-async: 2.6.2
+ schema-utils: 4.3.3
+ tapable: 2.3.0
+ terser-webpack-plugin: 5.3.16(@swc/core@1.15.11)(webpack@5.104.1(@swc/core@1.15.11))
+ watchpack: 2.5.1
+ webpack-sources: 3.3.3
+ transitivePeerDependencies:
+ - '@swc/core'
+ - esbuild
+ - uglify-js
+
whatwg-mimetype@3.0.0: {}
whatwg-mimetype@4.0.0: {}