Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -41,3 +41,6 @@ storybook-static
|
||||
out/
|
||||
release/
|
||||
web-dist/
|
||||
|
||||
# Legacy Code (if applicable)
|
||||
legacy/
|
||||
@@ -33,6 +33,7 @@ The monorepo is organized into **Apps** (deployable applications) and **Packages
|
||||
│ └── docs-dev/ # Component Documentation & Playground (Storybook)
|
||||
│
|
||||
├── packages/
|
||||
│ ├── core-api/ # Shared HTTP Client, Observability & Data Services Engine
|
||||
│ ├── ui/ # Shared UI Component Library
|
||||
│ ├── utils/ # Shared Utilities (Date, Encryption, Core Logic, etc)
|
||||
│ └── configs/ # Shared Tooling Configurations
|
||||
@@ -204,7 +205,40 @@ An isolated environment for developing and documenting UI components.
|
||||
|
||||
---
|
||||
|
||||
### 5. `packages/utils`
|
||||
### 5. `packages/core-api`
|
||||
|
||||
The **platform-agnostic API engine** for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine.
|
||||
|
||||
* Consumed by `apps/web`, `apps/landing`, and any future workspace
|
||||
* Centralizes all `@grafana/faro-*` and `@opentelemetry/*` dependencies
|
||||
* Provides plug-and-play telemetry via `initTelemetry()` + `faroAdapter`
|
||||
|
||||
**Tech Stack**:
|
||||
|
||||
* Axios (isolated instances, zero singleton pollution)
|
||||
* Grafana Faro (RUM, Logs, Error tracking)
|
||||
* OpenTelemetry (custom spans, distributed tracing)
|
||||
* TypeScript (strict types, module augmentation)
|
||||
|
||||
**Key Capabilities**:
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| 🏭 HTTP Client Factory | `createHttpClient()` — per-app isolated Axios instances with interceptor hooks |
|
||||
| 📡 Faro/Loki Baseline | Every request automatically pushes structured logs with `module.key` and `module.action` |
|
||||
| 🎯 Custom Spans (Opt-In) | `telemetryContext.customSpanName` creates explicit OTel spans visible in Grafana Tempo |
|
||||
| 🛡️ Error Normalization | `ApiError.fromAxiosError()` — structured, serializable error codes for all failure modes |
|
||||
| 📦 Data Services Engine | `CommonRemoteDataServices` — full CRUD + lifecycle operations with zero boilerplate |
|
||||
|
||||
**Documentation**:
|
||||
|
||||
| Document | Contents |
|
||||
|---|---|
|
||||
| [README.md](packages/core-api/README.md) | Architecture, HTTP client setup, observability strategy, data services, app integration guide |
|
||||
|
||||
---
|
||||
|
||||
### 6. `packages/utils`
|
||||
|
||||
Shared business logic and reusable utility modules that can be consumed across multiple applications. Fully tested using Vitest.
|
||||
|
||||
@@ -212,7 +246,7 @@ This package is intended to hold non-UI, cross-cutting logic such as date/time h
|
||||
|
||||
---
|
||||
|
||||
### 6. `packages/ui`
|
||||
### 7. `packages/ui`
|
||||
|
||||
Shared UI component library (Buttons, Inputs, Cards, Layouts).
|
||||
|
||||
@@ -221,7 +255,7 @@ Shared UI component library (Buttons, Inputs, Cards, Layouts).
|
||||
|
||||
---
|
||||
|
||||
### 7. `packages/configs`
|
||||
### 8. `packages/configs`
|
||||
|
||||
Single source of truth for tooling configuration.
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-api": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ThemeProvider } from '@repo/ui/provider';
|
||||
import { Button } from '@repo/ui/components';
|
||||
import LandingSample from './features/public-content/presentation/LandingSample';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -31,6 +32,10 @@ export default function App() {
|
||||
✅ @repo/ui workspace link verified — Button component rendered successfully.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Enterprise Web App</h1>
|
||||
<LandingSample />
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { publicClient } from '../../../lib/api-client';
|
||||
|
||||
// ─── Domain Entity ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Public content entity for the landing page.
|
||||
*
|
||||
* Represents promotional content, blog posts, or announcements
|
||||
* served from a public API without authentication.
|
||||
*/
|
||||
export interface PublicContentEntity extends BaseEntity {
|
||||
title: string;
|
||||
slug: string;
|
||||
excerpt: string;
|
||||
imageUrl: string;
|
||||
publishedAt: string;
|
||||
}
|
||||
|
||||
// ─── Data Services Instance ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Public content data services — wired to the lightweight `publicClient`.
|
||||
*
|
||||
* No auth, no OTel — pure zero-overhead HTTP calls.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { data } = await publicContentServices.getMany();
|
||||
* const { data: post } = await publicContentServices.getOne('hello-world');
|
||||
* ```
|
||||
*/
|
||||
export const publicContentServices = new CommonRemoteDataServices<PublicContentEntity>(
|
||||
publicClient,
|
||||
{
|
||||
apiUrl: '/content',
|
||||
moduleKey: 'PUBLIC_CONTENT',
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState } from 'react';
|
||||
import { publicContentServices } from '../data/public.data-services';
|
||||
import type { PublicContentEntity } from '../data/public.data-services';
|
||||
import type { ApiResponse } from '@repo/core-api/http-client';
|
||||
import { ApiError } from '@repo/core-api/errors';
|
||||
|
||||
/**
|
||||
* Sample component demonstrating the `@repo/core-api` integration
|
||||
* for the Landing App with TelemetryContext escape hatch.
|
||||
*
|
||||
* Pipeline: Faro auto-instrumentation → No Auth → GET /content
|
||||
* + Custom span "public.content.fetch" with enriched tags
|
||||
*/
|
||||
export default function LandingSample() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<ApiResponse<PublicContentEntity[]> | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFetch = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const response = await publicContentServices.getMany<PublicContentEntity[]>({
|
||||
// ── Telemetry Escape Hatch ──────────────────────────────
|
||||
// Even the lightweight landing app can push custom spans
|
||||
// and business events when needed.
|
||||
telemetryContext: {
|
||||
customSpanName: 'public.content.fetch',
|
||||
tags: {
|
||||
'feature': 'landing',
|
||||
'ui.component': 'LandingSample',
|
||||
'ui.action': 'content_fetch',
|
||||
},
|
||||
pushEventOnSuccess: 'public_content_loaded',
|
||||
},
|
||||
});
|
||||
setResult(response);
|
||||
console.log('[LandingSample] Response:', response);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(`[${err.code}] ${err.message} (HTTP ${err.status})`);
|
||||
console.error('[LandingSample] ApiError:', err.toJSON());
|
||||
} else {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, fontFamily: 'monospace' }}>
|
||||
<h2>🧪 Public Content Services — Integration Test</h2>
|
||||
<p style={{ color: '#888', fontSize: 14 }}>
|
||||
Pipeline: Faro + Custom Span "public.content.fetch" → No Auth → GET /content
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleFetch}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
fontSize: 16,
|
||||
cursor: loading ? 'wait' : 'pointer',
|
||||
background: loading ? '#555' : '#059669',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
{loading ? 'Fetching…' : 'Test Public Fetch'}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<pre style={{ color: '#ef4444', marginTop: 16, whiteSpace: 'pre-wrap' }}>
|
||||
❌ {error}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<pre style={{ marginTop: 16, background: '#1e1e2e', color: '#a6e3a1', padding: 16, borderRadius: 8, overflow: 'auto' }}>
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createHttpClient } from '@repo/core-api/http-client';
|
||||
import { faroAdapter } from '@repo/core-api/observability';
|
||||
|
||||
/**
|
||||
* Lightweight public HTTP client for `apps/landing`.
|
||||
*
|
||||
* - Uses the shared Faro adapter (initialized via `initTelemetry()` in main.tsx)
|
||||
* - No auth token injection
|
||||
* - Minimal config for maximum performance
|
||||
*
|
||||
* Designed for public-facing content that requires no authentication.
|
||||
*/
|
||||
export const publicClient = createHttpClient({
|
||||
baseURL: import.meta.env.VITE_API_URL ?? 'https://api.eigen.co/public/v1',
|
||||
timeout: 10000,
|
||||
observability: faroAdapter,
|
||||
});
|
||||
@@ -1,3 +1,17 @@
|
||||
// ─── Optional Telemetry Bootstrap ───────────────────────────────
|
||||
// Landing app uses minimal telemetry. Remove this block entirely
|
||||
// if you want zero observability overhead.
|
||||
import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||
|
||||
initTelemetry({
|
||||
appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-landing',
|
||||
appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0',
|
||||
telemetryUrl: import.meta.env.VITE_FARO_URL || 'https://telemetry.eigen.co.id/collect',
|
||||
environment: import.meta.env.VITE_ENV || 'development',
|
||||
// No otlpTraceUrl — only Faro collector, minimal overhead
|
||||
});
|
||||
|
||||
// ─── Application Bootstrap ──────────────────────────────────────
|
||||
import './main.css';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -13,6 +13,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-api": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -33,7 +33,8 @@ export default function App() {
|
||||
<Route path="/403" element={<Forbidden />} />
|
||||
<Route path="/maintenance" element={<Maintenance />} />
|
||||
<Route path="/coming-soon" element={<ComingSoon />} />
|
||||
<Route path="/" element={<Navigate to="/showcase" />} />
|
||||
{/* <Route path="/" element={<Navigate to="/showcase" />} /> */}
|
||||
<Route path="/" element={<Navigate to="/app" />} />
|
||||
<Route path="*" element={<Navigate to="/404" />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import BookingSample from "./features/booking/presentation/BookingSample";
|
||||
|
||||
export default function ExamplePage() {
|
||||
return <div className="bg-amber-200">example</div>;
|
||||
return <div className="bg-amber-200">example
|
||||
<div className="p-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Enterprise Web App</h1>
|
||||
<BookingSample />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { apiClient } from '../../../../../../lib/api-client';
|
||||
|
||||
// ─── Domain Entity ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Booking domain entity.
|
||||
*
|
||||
* In a real module, this would be defined in the domain layer
|
||||
* (e.g., `features/booking/domain/entities.ts`) and imported here.
|
||||
*/
|
||||
export interface BookingEntity extends BaseEntity {
|
||||
bookingCode: string;
|
||||
customerName: string;
|
||||
checkInDate: string;
|
||||
checkOutDate: string;
|
||||
status: 'pending' | 'confirmed' | 'cancelled';
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
// ─── Data Services Instance ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Booking data services — wired to the enterprise `apiClient`.
|
||||
*
|
||||
* All requests flow through the full interceptor chain:
|
||||
* Faro tracing → Bearer token injection → ApiError normalization.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { data } = await bookingServices.getMany({ params: { page: 1 } });
|
||||
* const { data: booking } = await bookingServices.getOne('42');
|
||||
* await bookingServices.confirmProcessTransaction('42');
|
||||
* ```
|
||||
*/
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||
apiClient,
|
||||
{
|
||||
apiUrl: '/bookings',
|
||||
moduleKey: 'BOOKING',
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useState } from 'react';
|
||||
import { bookingServices } from '../data/booking.data-services';
|
||||
import type { BookingEntity } from '../data/booking.data-services';
|
||||
import type { ApiResponse } from '@repo/core-api/http-client';
|
||||
import { ApiError } from '@repo/core-api/errors';
|
||||
|
||||
/**
|
||||
* Sample component demonstrating `@repo/core-api` integration
|
||||
* with the advanced TelemetryContext escape hatch.
|
||||
*
|
||||
* Pipeline: Faro auto-instrumentation → Bearer token → GET /bookings
|
||||
* + Custom span "booking.list.fetch" with enriched tags
|
||||
*/
|
||||
export default function BookingSample() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<ApiResponse<BookingEntity[]> | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFetch = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const response = await bookingServices.getMany<BookingEntity[]>({
|
||||
params: { page: 1, limit: 20 },
|
||||
// ── Telemetry Escape Hatch ──────────────────────────────
|
||||
// This creates a custom OTel span named "booking.list.fetch",
|
||||
// attaches business tags, and pushes a Faro event on success.
|
||||
telemetryContext: {
|
||||
customSpanName: 'booking.list.fetch',
|
||||
tags: {
|
||||
'feature': 'booking',
|
||||
'ui.component': 'BookingSample',
|
||||
'ui.action': 'list_fetch',
|
||||
'page': 1,
|
||||
},
|
||||
pushEventOnSuccess: 'booking_list_loaded',
|
||||
},
|
||||
});
|
||||
setResult(response);
|
||||
console.log('[BookingSample] Response:', response);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(`[${err.code}] ${err.message} (HTTP ${err.status})`);
|
||||
console.error('[BookingSample] ApiError:', err.toJSON());
|
||||
} else {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, fontFamily: 'monospace' }}>
|
||||
<h2>🧪 Booking Data Services — Integration Test</h2>
|
||||
<p style={{ color: '#888', fontSize: 14 }}>
|
||||
Pipeline: Faro + Custom Span "booking.list.fetch" → Bearer Token → GET /bookings
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleFetch}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
fontSize: 16,
|
||||
cursor: loading ? 'wait' : 'pointer',
|
||||
background: loading ? '#555' : '#4f46e5',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
{loading ? 'Fetching…' : 'Test Fetch Bookings'}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<pre style={{ color: '#ef4444', marginTop: 16, whiteSpace: 'pre-wrap' }}>
|
||||
❌ {error}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<pre style={{ marginTop: 16, background: '#1e1e2e', color: '#a6e3a1', padding: 16, borderRadius: 8, overflow: 'auto' }}>
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createHttpClient } from '@repo/core-api/http-client';
|
||||
import { faroAdapter } from '@repo/core-api/observability';
|
||||
|
||||
/**
|
||||
* Enterprise HTTP client for `apps/web`.
|
||||
*
|
||||
* - Full Faro observability via the shared `faroAdapter`
|
||||
* - Automatic Bearer token injection from localStorage
|
||||
* - 401 redirect to `/auth/login`
|
||||
* - Supports per-request `telemetryContext` for custom spans/tags
|
||||
*
|
||||
* All interceptors (auth, observability, error normalization)
|
||||
* are baked into this instance. Import this singleton throughout
|
||||
* the web application — never create raw axios instances.
|
||||
*/
|
||||
export const apiClient = createHttpClient(
|
||||
{
|
||||
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||
timeout: 15000,
|
||||
observability: faroAdapter,
|
||||
},
|
||||
{
|
||||
// ── Auth Interceptor ──────────────────────────────────────────
|
||||
onRequest: async (config) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
// ── Error Interceptor ─────────────────────────────────────────
|
||||
onResponseError: async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Clear stale token and redirect to login
|
||||
localStorage.removeItem('access_token');
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
);
|
||||
+14
-7
@@ -1,15 +1,22 @@
|
||||
// import { DateUtils } from '@repo/utils';
|
||||
// import { CurrencyUtils } from '@repo/utils';
|
||||
// ─── Observability Bootstrap (MUST be first) ────────────────────
|
||||
// Initializes Grafana Faro + OTel auto-instrumentation before any
|
||||
// React code or HTTP requests are executed.
|
||||
import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||
|
||||
initTelemetry({
|
||||
appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-web',
|
||||
appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0',
|
||||
telemetryUrl: import.meta.env.VITE_FARO_URL || 'https://telemetry.eigen.co.id/collect',
|
||||
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || 'https://telemetry.eigen.co.id/v1/traces',
|
||||
environment: import.meta.env.VITE_ENV || 'development',
|
||||
});
|
||||
|
||||
// ─── Application Bootstrap ──────────────────────────────────────
|
||||
import './main.css';
|
||||
import { lazy, StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
const App = lazy(() => import('./apps'));
|
||||
|
||||
// DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||
// DateUtils.setGlobalConfig('Asia/Makassar');
|
||||
|
||||
// CurrencyUtils.setGlobalPrefix('IDR ');
|
||||
// CurrencyUtils.setGlobalDecimalSeparator(',');
|
||||
createRoot(document.getElementById('app')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
# @repo/core-api
|
||||
|
||||
The platform-agnostic API engine for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine — consumed by `apps/web`, `apps/landing`, and any future workspace.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [HTTP Client](#http-client)
|
||||
- [Observability](#observability)
|
||||
- [Data Services](#data-services)
|
||||
- [Application Setup Guide](#application-setup-guide)
|
||||
- [Per-Request Telemetry (Escape Hatch)](#per-request-telemetry-escape-hatch)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Package Exports](#package-exports)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ @repo/core-api │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
|
||||
│ │ http-client │ │ observability │ │ data-services │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ createHttp │◄──│ faroAdapter │ │ BaseRemoteData │ │
|
||||
│ │ Client() │ │ initTelemetry() │ │ Services │ │
|
||||
│ │ │ │ getFaro() │ │ CommonRemoteData │ │
|
||||
│ │ ApiResponse │ │ noopAdapter │ │ Services │ │
|
||||
│ └──────┬───────┘ └───────────────────┘ └────────┬──────────┘ │
|
||||
│ │ │ │
|
||||
│ └────────────────────┬───────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────┴────────┐ │
|
||||
│ │ errors │ │
|
||||
│ │ ApiError │ │
|
||||
│ │ ErrorCodes │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
apps/web apps/landing apps/desktop
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
Every HTTP request flows through this pipeline:
|
||||
|
||||
```
|
||||
Component → DataService.getMany() → execute()
|
||||
→ httpClient.request()
|
||||
→ Request Interceptor:
|
||||
1. faroAdapter.onRequestStart() ← Faro log + optional custom span
|
||||
2. hooks.onRequest() ← App-specific (e.g., Bearer token)
|
||||
→ Network (fetch/XHR)
|
||||
→ Response Interceptor:
|
||||
SUCCESS: faroAdapter.onRequestEnd() → hooks.onResponse()
|
||||
ERROR: faroAdapter.onRequestError() → hooks.onResponseError()
|
||||
→ ApiError.fromAxiosError()
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Observability adapter errors are **caught internally** via try-catch in the interceptor chain. An adapter crash will never swallow or replace the original API error — the UI always receives the correct rejection.
|
||||
|
||||
---
|
||||
|
||||
## HTTP Client
|
||||
|
||||
### `createHttpClient(config, hooks?)`
|
||||
|
||||
Creates an **isolated** Axios instance. Each app receives its own interceptor chain — no globals are shared or mutated.
|
||||
|
||||
```typescript
|
||||
import { createHttpClient } from '@repo/core-api/http-client';
|
||||
import { faroAdapter } from '@repo/core-api/observability';
|
||||
|
||||
export const apiClient = createHttpClient(
|
||||
{
|
||||
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||
timeout: 15000,
|
||||
observability: faroAdapter,
|
||||
},
|
||||
{
|
||||
onRequest: async (config) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
},
|
||||
onResponseError: async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('access_token');
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `baseURL` | `string` | *required* | Base URL for all requests |
|
||||
| `timeout` | `number` | `15000` | Default request timeout (ms) |
|
||||
| `defaultHeaders` | `Record<string, string>` | `{}` | Headers applied to every request |
|
||||
| `observability` | `IObservabilityAdapter` | `noopAdapter` | Observability adapter (Faro or no-op) |
|
||||
|
||||
### Interceptor Hooks
|
||||
|
||||
| Hook | Signature | Purpose |
|
||||
|---|---|---|
|
||||
| `onRequest` | `(config) => config` | Inject auth tokens, tenant headers |
|
||||
| `onResponse` | `(response) => response` | Transform response shapes |
|
||||
| `onResponseError` | `(error) => never` | App-specific error handling (e.g., 401 redirect) |
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
### Strategy: Opt-In Custom Spans + Faro/Loki Baseline
|
||||
|
||||
The observability layer operates in two complementary modes:
|
||||
|
||||
| Mode | Activation | What it does |
|
||||
|---|---|---|
|
||||
| **Baseline** (always on) | Automatic | Pushes structured logs to Faro/Loki on every request with `module.key`, `module.action`, HTTP method, and URL |
|
||||
| **Custom Span** (opt-in) | Via `telemetryContext.customSpanName` | Creates an explicit OTel span with custom tags, visible in Grafana Tempo |
|
||||
|
||||
> [!NOTE]
|
||||
> `trace.getActiveSpan()` returns `undefined` inside Axios interceptors due to browser XHR/Fetch lifecycle race conditions with Faro's `TracingInstrumentation`. The adapter does **not** attempt to enrich auto-instrumented spans. HTTP span capture is handled entirely by `TracingInstrumentation` auto-instrumentation.
|
||||
|
||||
### Initialization
|
||||
|
||||
Call `initTelemetry()` **once** at the top of your app's entry point, before any React code:
|
||||
|
||||
```typescript
|
||||
import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||
|
||||
initTelemetry({
|
||||
appName: 'fe-monorepo-web',
|
||||
appVersion: '1.0.0',
|
||||
telemetryUrl: 'https://telemetry.eigen.co.id/collect',
|
||||
environment: 'production',
|
||||
// Optional: direct OTLP export to Grafana Tempo
|
||||
otlpTraceUrl: 'https://telemetry.eigen.co.id/v1/traces',
|
||||
});
|
||||
```
|
||||
|
||||
### `TelemetryConfig`
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `appName` | `string` | ✅ | Application name for Faro + OTel resource attributes |
|
||||
| `appVersion` | `string` | ✅ | SemVer version |
|
||||
| `telemetryUrl` | `string` | ✅ | Grafana Faro collector URL |
|
||||
| `environment` | `string` | ✅ | Deployment environment (`production`, `staging`, `development`) |
|
||||
| `otlpTraceUrl` | `string` | — | Separate OTLP trace endpoint for direct Tempo ingestion |
|
||||
| `propagateTraceHeaderCorsUrls` | `Array<string \| RegExp>` | — | CORS patterns for W3C trace context propagation (default: `[/.*/]`) |
|
||||
|
||||
### Audit Headers
|
||||
|
||||
Every request dispatched through `BaseRemoteDataServices` automatically attaches two business audit headers:
|
||||
|
||||
| Header | Source | Purpose |
|
||||
|---|---|---|
|
||||
| `ex-module-key` | `DataServicesConfig.moduleKey` | Identifies the business module (e.g., `BOOKING`) |
|
||||
| `ex-module-action` | `RequestDescriptor.action` | Identifies the operation (e.g., `READ`, `CREATE`) |
|
||||
|
||||
These headers are extracted by the `faroAdapter` and included in all Faro `pushLog`, `pushError`, and `pushEvent` calls as top-level context — making them directly queryable in **LogQL (Loki)**.
|
||||
|
||||
### Span Safety Guarantees
|
||||
|
||||
| Guarantee | Mechanism |
|
||||
|---|---|
|
||||
| **No span leaks** | `safeEndSpan()` always closes the span and detaches the reference from config |
|
||||
| **No double-close on retry** | Span reference is deleted from config after `span.end()` |
|
||||
| **No error swallowing** | All adapter calls are wrapped in try-catch in `create-http-client.ts` |
|
||||
| **No crash on timeout** | `null`/`undefined` config guards on all `error.config` access |
|
||||
|
||||
---
|
||||
|
||||
## Data Services
|
||||
|
||||
### `CommonRemoteDataServices<E>`
|
||||
|
||||
A concrete, ready-to-use data services class that provides full CRUD and lifecycle operations. Extends `BaseRemoteDataServices<E>`.
|
||||
|
||||
```typescript
|
||||
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
|
||||
interface BookingEntity extends BaseEntity {
|
||||
bookingCode: string;
|
||||
customerName: string;
|
||||
status: 'pending' | 'confirmed' | 'cancelled';
|
||||
}
|
||||
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||
apiClient,
|
||||
{
|
||||
apiUrl: '/bookings',
|
||||
moduleKey: 'BOOKING',
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Available Operations
|
||||
|
||||
| Method | HTTP | URL Template | Description |
|
||||
|---|---|---|---|
|
||||
| `getMany(config?)` | GET | `/bookings` | Fetch paginated list |
|
||||
| `getOne(id, config?)` | GET | `/bookings/:id` | Fetch single entity |
|
||||
| `create(data, config?)` | POST | `/bookings` | Create new entity |
|
||||
| `edit(id, data, config?)` | PUT | `/bookings/:id` | Update entity |
|
||||
| `delete(id, config?)` | DELETE | `/bookings/:id` | Delete entity |
|
||||
| `batchDelete(ids, config?)` | DELETE | `/bookings/batch` | Delete multiple |
|
||||
| `activate(id)` | PATCH | `/bookings/:id/activate` | Activate entity |
|
||||
| `deactivate(id)` | PATCH | `/bookings/:id/deactivate` | Deactivate entity |
|
||||
| `confirmProcessData(id)` | PATCH | `/bookings/:id/confirm-process-data` | Confirm data processing |
|
||||
| `confirmProcessTransaction(id)` | PATCH | `/bookings/:id/confirm-process-transaction` | Confirm transaction |
|
||||
| `cancelProcessTransaction(id)` | PATCH | `/bookings/:id/cancel-process-transaction` | Cancel transaction |
|
||||
| `rollbackProcessTransaction(id)` | PATCH | `/bookings/:id/rollback-process-transaction` | Rollback transaction |
|
||||
| `holdProcessTransaction(id)` | PATCH | `/bookings/:id/hold-process-transaction` | Hold transaction |
|
||||
|
||||
All batch variants (`batchActivate`, `batchDeactivate`, etc.) are also available.
|
||||
|
||||
### Escape Hatch: `customRequest<T>(config)`
|
||||
|
||||
For non-standard endpoints that don't fit the CRUD pattern:
|
||||
|
||||
```typescript
|
||||
const taxResult = await bookingServices.customRequest<TaxCalculation>({
|
||||
url: '/bookings/42/calculate-tax',
|
||||
method: 'POST',
|
||||
data: { items: [...] },
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Application Setup Guide
|
||||
|
||||
### 1. Initialize Telemetry (Entry Point)
|
||||
|
||||
```typescript
|
||||
// apps/web/src/main.tsx — MUST be the first import
|
||||
import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||
|
||||
initTelemetry({
|
||||
appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-web',
|
||||
appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0',
|
||||
telemetryUrl: import.meta.env.VITE_FARO_URL || 'https://telemetry.eigen.co.id/collect',
|
||||
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || 'https://telemetry.eigen.co.id/v1/traces',
|
||||
environment: import.meta.env.VITE_ENV || 'development',
|
||||
});
|
||||
|
||||
// ... rest of React bootstrap
|
||||
```
|
||||
|
||||
### 2. Create the HTTP Client
|
||||
|
||||
```typescript
|
||||
// apps/web/src/lib/api-client.ts
|
||||
import { createHttpClient } from '@repo/core-api/http-client';
|
||||
import { faroAdapter } from '@repo/core-api/observability';
|
||||
|
||||
export const apiClient = createHttpClient({
|
||||
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||
timeout: 15000,
|
||||
observability: faroAdapter,
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Create a Data Service
|
||||
|
||||
```typescript
|
||||
// features/booking/data/booking.data-services.ts
|
||||
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
|
||||
export interface BookingEntity extends BaseEntity {
|
||||
bookingCode: string;
|
||||
customerName: string;
|
||||
status: 'pending' | 'confirmed' | 'cancelled';
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||
apiClient,
|
||||
{ apiUrl: '/bookings', moduleKey: 'BOOKING' },
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Consume in a React Component
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
import { bookingServices } from '../data/booking.data-services';
|
||||
import type { BookingEntity } from '../data/booking.data-services';
|
||||
import type { ApiResponse } from '@repo/core-api/http-client';
|
||||
import { ApiError } from '@repo/core-api/errors';
|
||||
|
||||
export default function BookingSample() {
|
||||
const [result, setResult] = useState<ApiResponse<BookingEntity[]> | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFetch = async () => {
|
||||
try {
|
||||
const response = await bookingServices.getMany<BookingEntity[]>({
|
||||
params: { page: 1, limit: 20 },
|
||||
// Optional: Per-request telemetry escape hatch
|
||||
telemetryContext: {
|
||||
customSpanName: 'booking.list.fetch',
|
||||
tags: { feature: 'booking', page: 1 },
|
||||
pushEventOnSuccess: 'booking_list_loaded',
|
||||
},
|
||||
});
|
||||
setResult(response);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(`[${err.code}] ${err.message} (HTTP ${err.status})`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={handleFetch}>Fetch Bookings</button>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Per-Request Telemetry (Escape Hatch)
|
||||
|
||||
### `TelemetryContext`
|
||||
|
||||
Attach to any request via the `telemetryContext` property to push custom spans and business events:
|
||||
|
||||
```typescript
|
||||
interface TelemetryContext {
|
||||
/** Creates a custom OTel span wrapping this request (visible in Grafana Tempo). */
|
||||
customSpanName?: string;
|
||||
/** Custom tags enriching the span and Faro logs (prefixed with `custom.` on spans). */
|
||||
tags?: Record<string, string | number | boolean>;
|
||||
/** Pushes a named Faro event on success (visible in Grafana Faro dashboard). */
|
||||
pushEventOnSuccess?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Precedence
|
||||
|
||||
`telemetryContext` can be provided at two levels. The top-level `ExecuteOptions.telemetryContext` takes precedence over `config.telemetryContext`:
|
||||
|
||||
```typescript
|
||||
// Top-level (preferred)
|
||||
await bookingServices.getMany({
|
||||
telemetryContext: { customSpanName: 'booking.list.fetch' },
|
||||
});
|
||||
|
||||
// Nested in config (also works)
|
||||
await bookingServices.getMany({
|
||||
params: { page: 1 },
|
||||
telemetryContext: { customSpanName: 'booking.list.fetch' },
|
||||
});
|
||||
```
|
||||
|
||||
### What Happens at Each Stage
|
||||
|
||||
| Stage | Baseline (no telemetryContext) | With `customSpanName` |
|
||||
|---|---|---|
|
||||
| **Request Start** | Faro `pushLog` (DEBUG) with `module.key`, `module.action`, URL | + Creates OTel span with `http.method`, `http.url`, `custom.*` tags |
|
||||
| **Request Success** | — | Closes span (OK). If `pushEventOnSuccess`, pushes Faro event |
|
||||
| **Request Error** | Faro `pushError` + `pushLog` (ERROR) | + Closes span (ERROR), records exception |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### `ApiError`
|
||||
|
||||
All non-2xx responses are normalized into structured `ApiError` instances:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await bookingServices.getOne('42');
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
err.code; // ApiErrorCode.NOT_FOUND
|
||||
err.status; // 404
|
||||
err.message; // "Booking not found"
|
||||
err.data; // Raw server response body
|
||||
err.toJSON(); // Serializable for logging
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|---|---|---|
|
||||
| `BAD_REQUEST` | 400 | Invalid request parameters |
|
||||
| `UNAUTHORIZED` | 401 | Missing or expired token |
|
||||
| `FORBIDDEN` | 403 | Insufficient permissions |
|
||||
| `NOT_FOUND` | 404 | Resource not found |
|
||||
| `TIMEOUT` | — | Request timed out (`ECONNABORTED`) |
|
||||
| `CANCELLED` | — | Request was cancelled (`ERR_CANCELED`) |
|
||||
| `NETWORK_ERROR` | — | No response received |
|
||||
| `SERVER_ERROR` | 500+ | Internal server error |
|
||||
|
||||
---
|
||||
|
||||
## Package Exports
|
||||
|
||||
| Import Path | Contents |
|
||||
|---|---|
|
||||
| `@repo/core-api/http-client` | `createHttpClient`, `ApiResponse`, `TelemetryContext`, Axios type re-exports |
|
||||
| `@repo/core-api/observability` | `faroAdapter`, `noopObservabilityAdapter`, `IObservabilityAdapter`, `initTelemetry`, `getFaro`, `TelemetryConfig` |
|
||||
| `@repo/core-api/observability/setup` | `initTelemetry`, `getFaro`, `TelemetryConfig` |
|
||||
| `@repo/core-api/data-services` | `BaseRemoteDataServices`, `CommonRemoteDataServices`, types, constants |
|
||||
| `@repo/core-api/errors` | `ApiError`, `ApiErrorCode` |
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@repo/core-api",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./http-client": "./src/http-client/index.ts",
|
||||
"./observability": "./src/observability/index.ts",
|
||||
"./observability/setup": "./src/observability/setup.ts",
|
||||
"./data-services": "./src/data-services/index.ts",
|
||||
"./errors": "./src/errors/index.ts"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint \"**/*.ts\"",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest --watch",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/faro-react": "^2.1.0",
|
||||
"@grafana/faro-web-sdk": "^2.1.0",
|
||||
"@grafana/faro-web-tracing": "^2.1.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.2.0",
|
||||
"axios": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"eslint": "^8.57.1",
|
||||
"typescript": "5.5.4",
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import type {
|
||||
BaseEntity,
|
||||
ApiURLMap,
|
||||
RequestMethodMap,
|
||||
RequestDescriptor,
|
||||
ExecuteOptions,
|
||||
DataServicesConfig,
|
||||
} from './types';
|
||||
import type { ApiResponse } from '../http-client/types';
|
||||
import { interpolateUrl } from './url-builder';
|
||||
import { DEFAULT_METHODS, DESCRIPTORS, makeDefaultURLs } from './constants';
|
||||
|
||||
/**
|
||||
* Abstract base class for remote data services.
|
||||
*
|
||||
* Provides a generic `execute()` method that eliminates the 22
|
||||
* near-identical methods from the legacy `BaseRemoteDataServices`.
|
||||
* Each operation is reduced to a one-liner calling `execute()`
|
||||
* with the appropriate descriptor.
|
||||
*
|
||||
* **Key architectural differences from legacy:**
|
||||
* - Receives an **injected** AxiosInstance (no global axios import)
|
||||
* - Returns `Promise<ApiResponse<T>>` (no callback-based onSuccess/onFailed)
|
||||
* - All operations are fully typed end-to-end
|
||||
* - Includes `customRequest<T>()` as an escape hatch for non-standard endpoints
|
||||
*
|
||||
* @typeParam E - The domain entity type (must extend BaseEntity)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class BookingDataServices extends BaseRemoteDataServices<BookingEntity> {}
|
||||
*
|
||||
* const services = new BookingDataServices(apiClient, {
|
||||
* apiUrl: '/bookings',
|
||||
* moduleKey: 'BOOKING',
|
||||
* });
|
||||
*
|
||||
* const { data, status } = await services.getOne<BookingEntity>('42');
|
||||
* ```
|
||||
*/
|
||||
export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity> {
|
||||
/** The injected, isolated HTTP client instance. */
|
||||
protected readonly httpClient: AxiosInstance;
|
||||
|
||||
/** Resolved URL map for all operations. */
|
||||
protected readonly urls: ApiURLMap;
|
||||
|
||||
/** Resolved HTTP method map for all operations. */
|
||||
protected readonly methods: RequestMethodMap;
|
||||
|
||||
/** Module key for the 'ex-module-key' audit header. */
|
||||
protected readonly moduleKey: string | undefined;
|
||||
|
||||
constructor(httpClient: AxiosInstance, config: DataServicesConfig) {
|
||||
this.httpClient = httpClient;
|
||||
this.moduleKey = config.moduleKey;
|
||||
|
||||
this.urls = {
|
||||
...makeDefaultURLs(config.apiUrl ?? ''),
|
||||
...(config.urls ?? {}),
|
||||
};
|
||||
|
||||
this.methods = {
|
||||
...DEFAULT_METHODS,
|
||||
...(config.methods ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Generic Executor ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The single generic request executor.
|
||||
*
|
||||
* All standard operations delegate to this method with a
|
||||
* pre-defined descriptor. This is the engine that replaces
|
||||
* 22 near-identical legacy methods.
|
||||
*
|
||||
* @typeParam T - Expected response data type
|
||||
* @param descriptor - Defines which URL, method, and action to use
|
||||
* @param options - Dynamic URL params and additional Axios config
|
||||
* @returns Typed API response with data and status
|
||||
*/
|
||||
protected async execute<T = unknown>(
|
||||
descriptor: RequestDescriptor,
|
||||
options?: ExecuteOptions,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const { urlKey, methodKey, action } = descriptor;
|
||||
|
||||
const response = await this.httpClient.request<T>({
|
||||
url: interpolateUrl(this.urls[urlKey], options?.variableURL),
|
||||
method: this.methods[methodKey],
|
||||
...(options?.config ?? {}),
|
||||
headers: {
|
||||
...(this.moduleKey ? { 'ex-module-key': this.moduleKey } : {}),
|
||||
'ex-module-action': action,
|
||||
...(options?.config?.headers ?? {}),
|
||||
},
|
||||
telemetryContext: options?.telemetryContext ?? options?.config?.telemetryContext // FIXME,
|
||||
});
|
||||
|
||||
return { data: response.data, status: response.status };
|
||||
}
|
||||
|
||||
// ─── Escape Hatch ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Execute a fully custom request that doesn't fit standard CRUD.
|
||||
*
|
||||
* Use this for non-standard endpoints like `/calculate-tax`,
|
||||
* custom aggregations, or third-party integrations.
|
||||
*
|
||||
* The request still flows through the injected httpClient, so
|
||||
* all interceptors (auth, observability, error handling) are
|
||||
* preserved automatically.
|
||||
*
|
||||
* @typeParam T - Expected response data type
|
||||
* @param config - Complete Axios request configuration
|
||||
* @returns Typed API response with data and status
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const tax = await services.customRequest<TaxResult>({
|
||||
* url: '/bookings/42/calculate-tax',
|
||||
* method: 'POST',
|
||||
* data: { items: [...] },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async customRequest<T = unknown>(
|
||||
config: AxiosRequestConfig,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const response = await this.httpClient.request<T>({
|
||||
...config,
|
||||
headers: {
|
||||
...(this.moduleKey ? { 'ex-module-key': this.moduleKey } : {}),
|
||||
...(config.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
return { data: response.data, status: response.status };
|
||||
}
|
||||
|
||||
// ─── CRUD Operations ───────────────────────────────────────────
|
||||
|
||||
/** Fetch a paginated list of entities. */
|
||||
getMany<T = E[]>(config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.getMany, { config });
|
||||
}
|
||||
|
||||
/** Fetch a single entity by ID. */
|
||||
getOne<T = E>(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.getOne, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a new entity. */
|
||||
create<T = E>(data: Partial<E>, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.create, {
|
||||
config: { ...config, data },
|
||||
});
|
||||
}
|
||||
|
||||
/** Update an existing entity by ID. */
|
||||
edit<T = E>(id: string, data: Partial<E>, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.edit, {
|
||||
variableURL: { id },
|
||||
config: { ...config, data },
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a single entity by ID. */
|
||||
delete(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.delete, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete multiple entities by IDs. */
|
||||
batchDelete(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchDelete, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Activation Lifecycle ─────────────────────────────────────
|
||||
|
||||
/** Activate a single entity. */
|
||||
activate(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.activate, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Activate multiple entities. */
|
||||
batchActivate(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchActivate, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Deactivate a single entity. */
|
||||
deactivate(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.deactivate, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Deactivate multiple entities. */
|
||||
batchDeactivate(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchDeactivate, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Data Processing Lifecycle ────────────────────────────────
|
||||
|
||||
/** Confirm processing of a single data record. */
|
||||
confirmProcessData(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.confirmProcessData, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Confirm processing of multiple data records. */
|
||||
batchConfirmProcessData(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchConfirmProcessData, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel processing of a single data record. */
|
||||
cancelProcessData(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.cancelProcessData, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel processing of multiple data records. */
|
||||
batchCancelProcessData(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchCancelProcessData, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Transaction Lifecycle ────────────────────────────────────
|
||||
|
||||
/** Confirm a transaction. */
|
||||
confirmProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.confirmProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Confirm multiple transactions. */
|
||||
batchConfirmProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchConfirmProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel a transaction. */
|
||||
cancelProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.cancelProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel multiple transactions. */
|
||||
batchCancelProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchCancelProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Rollback a transaction. */
|
||||
rollbackProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.rollbackProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Rollback multiple transactions. */
|
||||
batchRollbackProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchRollbackProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Hold a transaction. */
|
||||
holdProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.holdProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Hold multiple transactions. */
|
||||
batchHoldProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchHoldProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { BaseEntity } from './types';
|
||||
import { BaseRemoteDataServices } from './base-remote.data-services';
|
||||
|
||||
/**
|
||||
* General-purpose remote data services.
|
||||
*
|
||||
* A concrete, non-abstract version of BaseRemoteDataServices that
|
||||
* can be instantiated directly for standard CRUD modules that don't
|
||||
* need additional custom methods.
|
||||
*
|
||||
* For modules requiring domain-specific operations beyond standard
|
||||
* CRUD + lifecycle, extend BaseRemoteDataServices instead and add
|
||||
* custom methods using `this.execute()` or `this.customRequest()`.
|
||||
*
|
||||
* @typeParam E - The domain entity type
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Direct instantiation for standard modules
|
||||
* const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||
* apiClient,
|
||||
* { apiUrl: '/bookings', moduleKey: 'BOOKING' },
|
||||
* );
|
||||
*
|
||||
* const { data } = await bookingServices.getMany();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // For modules needing custom operations, extend the base:
|
||||
* class InvoiceDataServices extends BaseRemoteDataServices<InvoiceEntity> {
|
||||
* async calculateTax(invoiceId: string) {
|
||||
* return this.customRequest<TaxResult>({
|
||||
* url: `/invoices/${invoiceId}/calculate-tax`,
|
||||
* method: 'POST',
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class CommonRemoteDataServices<
|
||||
E extends BaseEntity = BaseEntity,
|
||||
> extends BaseRemoteDataServices<E> {}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { RequestMethodMap, RequestDescriptor, ApiURLMap } from './types';
|
||||
|
||||
// ─── Request Actions ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Constants for the 'ex-module-action' header.
|
||||
* Maps to backend permission/audit checks.
|
||||
*/
|
||||
export const REQUEST_ACTION = {
|
||||
VIEW: 'VIEW',
|
||||
CREATE: 'CREATE',
|
||||
EDIT: 'EDIT',
|
||||
DELETE: 'DELETE',
|
||||
CONFIRM_DATA: 'CONFIRM_DATA',
|
||||
CANCEL_DATA: 'CANCEL_DATA',
|
||||
CONFIRM_PROCESS_TRANSACTION: 'CONFIRM_PROCESS_TRANSACTION',
|
||||
CANCEL_PROCESS_TRANSACTION: 'CANCEL_PROCESS_TRANSACTION',
|
||||
} as const;
|
||||
|
||||
// ─── Default HTTP Methods ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sensible REST defaults for all operations.
|
||||
* Can be overridden per data-services instance.
|
||||
*/
|
||||
export const DEFAULT_METHODS: RequestMethodMap = {
|
||||
getManyMethod: 'GET',
|
||||
getOneMethod: 'GET',
|
||||
createMethod: 'POST',
|
||||
editMethod: 'PUT',
|
||||
deleteMethod: 'DELETE',
|
||||
batchDeleteMethod: 'POST',
|
||||
|
||||
activateMethod: 'PATCH',
|
||||
batchActivateMethod: 'POST',
|
||||
deactivateMethod: 'PATCH',
|
||||
batchDeactivateMethod: 'POST',
|
||||
|
||||
confirmProcessDataMethod: 'PATCH',
|
||||
batchConfirmProcessDataMethod: 'POST',
|
||||
cancelProcessDataMethod: 'PATCH',
|
||||
batchCancelProcessDataMethod: 'POST',
|
||||
|
||||
confirmProcessTransactionMethod: 'PATCH',
|
||||
batchConfirmProcessTransactionMethod: 'POST',
|
||||
cancelProcessTransactionMethod: 'PATCH',
|
||||
batchCancelProcessTransactionMethod: 'POST',
|
||||
rollbackProcessTransactionMethod: 'PATCH',
|
||||
batchRollbackProcessTransactionMethod: 'POST',
|
||||
holdProcessTransactionMethod: 'PATCH',
|
||||
batchHoldProcessTransactionMethod: 'POST',
|
||||
};
|
||||
|
||||
// ─── Operation Descriptors ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Pre-defined descriptors for all standard operations.
|
||||
* Each descriptor maps an operation to its URL key, method key,
|
||||
* and action header — eliminating 22 boilerplate methods.
|
||||
*/
|
||||
export const DESCRIPTORS = {
|
||||
getMany: { urlKey: 'getManyUrl', methodKey: 'getManyMethod', action: REQUEST_ACTION.VIEW },
|
||||
getOne: { urlKey: 'getOneUrl', methodKey: 'getOneMethod', action: REQUEST_ACTION.VIEW },
|
||||
create: { urlKey: 'createUrl', methodKey: 'createMethod', action: REQUEST_ACTION.CREATE },
|
||||
edit: { urlKey: 'editUrl', methodKey: 'editMethod', action: REQUEST_ACTION.EDIT },
|
||||
delete: { urlKey: 'deleteUrl', methodKey: 'deleteMethod', action: REQUEST_ACTION.DELETE },
|
||||
batchDelete: { urlKey: 'batchDeleteUrl', methodKey: 'batchDeleteMethod', action: REQUEST_ACTION.DELETE },
|
||||
|
||||
activate: { urlKey: 'activateUrl', methodKey: 'activateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||
batchActivate: { urlKey: 'batchActivateUrl', methodKey: 'batchActivateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||
deactivate: { urlKey: 'deactivateUrl', methodKey: 'deactivateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||
batchDeactivate: { urlKey: 'batchDeactivateUrl', methodKey: 'batchDeactivateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||
|
||||
confirmProcessData: { urlKey: 'confirmProcessDataUrl', methodKey: 'confirmProcessDataMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||
batchConfirmProcessData: { urlKey: 'batchConfirmProcessDataUrl', methodKey: 'batchConfirmProcessDataMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||
cancelProcessData: { urlKey: 'cancelProcessDataUrl', methodKey: 'cancelProcessDataMethod', action: REQUEST_ACTION.CANCEL_DATA },
|
||||
batchCancelProcessData: { urlKey: 'batchCancelProcessDataUrl', methodKey: 'batchCancelProcessDataMethod', action: REQUEST_ACTION.CANCEL_DATA },
|
||||
|
||||
confirmProcessTransaction: { urlKey: 'confirmProcessTransactionUrl', methodKey: 'confirmProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||
batchConfirmProcessTransaction: { urlKey: 'batchConfirmProcessTransactionUrl', methodKey: 'batchConfirmProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||
cancelProcessTransaction: { urlKey: 'cancelProcessTransactionUrl', methodKey: 'cancelProcessTransactionMethod', action: REQUEST_ACTION.CANCEL_PROCESS_TRANSACTION },
|
||||
batchCancelProcessTransaction: { urlKey: 'batchCancelProcessTransactionUrl', methodKey: 'batchCancelProcessTransactionMethod', action: REQUEST_ACTION.CANCEL_PROCESS_TRANSACTION },
|
||||
rollbackProcessTransaction: { urlKey: 'rollbackProcessTransactionUrl', methodKey: 'rollbackProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||
batchRollbackProcessTransaction: { urlKey: 'batchRollbackProcessTransactionUrl', methodKey: 'batchRollbackProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||
holdProcessTransaction: { urlKey: 'holdProcessTransactionUrl', methodKey: 'holdProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||
batchHoldProcessTransaction: { urlKey: 'batchHoldProcessTransactionUrl', methodKey: 'batchHoldProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||
} as const satisfies Record<string, RequestDescriptor>;
|
||||
|
||||
// ─── Default URL Factory ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generates the complete API URL map from a base path.
|
||||
*
|
||||
* @param apiUrl - Base API path (e.g., '/bookings')
|
||||
* @returns Full ApiURLMap with all CRUD + lifecycle URLs
|
||||
*/
|
||||
export function makeDefaultURLs(apiUrl: string): ApiURLMap {
|
||||
return {
|
||||
getManyUrl: `${apiUrl}`,
|
||||
getOneUrl: `${apiUrl}/:id`,
|
||||
createUrl: `${apiUrl}`,
|
||||
editUrl: `${apiUrl}/:id`,
|
||||
|
||||
deleteUrl: `${apiUrl}/:id`,
|
||||
batchDeleteUrl: `${apiUrl}/batch-delete`,
|
||||
|
||||
activateUrl: `${apiUrl}/:id/active`,
|
||||
batchActivateUrl: `${apiUrl}/batch-active`,
|
||||
deactivateUrl: `${apiUrl}/:id/inactive`,
|
||||
batchDeactivateUrl: `${apiUrl}/batch-inactive`,
|
||||
|
||||
confirmProcessDataUrl: `${apiUrl}/:id/confirm`,
|
||||
batchConfirmProcessDataUrl: `${apiUrl}/batch-confirm`,
|
||||
cancelProcessDataUrl: `${apiUrl}/:id/cancel`,
|
||||
batchCancelProcessDataUrl: `${apiUrl}/batch-cancel`,
|
||||
|
||||
confirmProcessTransactionUrl: `${apiUrl}/:id/confirm-data`,
|
||||
batchConfirmProcessTransactionUrl: `${apiUrl}/batch-confirm-data`,
|
||||
cancelProcessTransactionUrl: `${apiUrl}/:id/cancel`,
|
||||
batchCancelProcessTransactionUrl: `${apiUrl}/batch-cancel`,
|
||||
rollbackProcessTransactionUrl: `${apiUrl}/:id/confirm-rollback`,
|
||||
batchRollbackProcessTransactionUrl: `${apiUrl}/batch-confirm-rollback`,
|
||||
holdProcessTransactionUrl: `${apiUrl}/:id/confirm-hold`,
|
||||
batchHoldProcessTransactionUrl: `${apiUrl}/batch-confirm-hold`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// ─── Classes ────────────────────────────────────────────────────
|
||||
export { BaseRemoteDataServices } from './base-remote.data-services';
|
||||
export { CommonRemoteDataServices } from './common-remote.data-services';
|
||||
|
||||
// ─── Utilities ──────────────────────────────────────────────────
|
||||
export { interpolateUrl } from './url-builder';
|
||||
|
||||
// ─── Constants ──────────────────────────────────────────────────
|
||||
export { REQUEST_ACTION, DEFAULT_METHODS, DESCRIPTORS, makeDefaultURLs } from './constants';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────
|
||||
export type {
|
||||
BaseEntity,
|
||||
ApiURLMap,
|
||||
RequestMethodMap,
|
||||
RequestDescriptor,
|
||||
ExecuteOptions,
|
||||
DataServicesConfig,
|
||||
} from './types';
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import type { TelemetryContext } from '../http-client/types';
|
||||
|
||||
// ─── Base Entity ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Minimal entity contract. All domain entities must have
|
||||
* an optional `id` field for CRUD operations.
|
||||
*/
|
||||
export interface BaseEntity {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
// ─── API URL Map ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Complete URL map for all standard CRUD and lifecycle operations.
|
||||
* Each key maps to a URL template string (e.g., '/bookings/:id').
|
||||
*/
|
||||
export interface ApiURLMap {
|
||||
getManyUrl: string;
|
||||
getOneUrl: string;
|
||||
createUrl: string;
|
||||
editUrl: string;
|
||||
deleteUrl: string;
|
||||
batchDeleteUrl: string;
|
||||
|
||||
activateUrl: string;
|
||||
batchActivateUrl: string;
|
||||
deactivateUrl: string;
|
||||
batchDeactivateUrl: string;
|
||||
|
||||
confirmProcessDataUrl: string;
|
||||
batchConfirmProcessDataUrl: string;
|
||||
cancelProcessDataUrl: string;
|
||||
batchCancelProcessDataUrl: string;
|
||||
|
||||
confirmProcessTransactionUrl: string;
|
||||
batchConfirmProcessTransactionUrl: string;
|
||||
cancelProcessTransactionUrl: string;
|
||||
batchCancelProcessTransactionUrl: string;
|
||||
rollbackProcessTransactionUrl: string;
|
||||
batchRollbackProcessTransactionUrl: string;
|
||||
holdProcessTransactionUrl: string;
|
||||
batchHoldProcessTransactionUrl: string;
|
||||
}
|
||||
|
||||
// ─── HTTP Method Map ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* HTTP method overrides for each operation.
|
||||
* Defaults to sensible REST conventions (GET, POST, PUT, DELETE, PATCH).
|
||||
*/
|
||||
export interface RequestMethodMap {
|
||||
getManyMethod: string;
|
||||
getOneMethod: string;
|
||||
createMethod: string;
|
||||
editMethod: string;
|
||||
deleteMethod: string;
|
||||
batchDeleteMethod: string;
|
||||
|
||||
activateMethod: string;
|
||||
batchActivateMethod: string;
|
||||
deactivateMethod: string;
|
||||
batchDeactivateMethod: string;
|
||||
|
||||
confirmProcessDataMethod: string;
|
||||
batchConfirmProcessDataMethod: string;
|
||||
cancelProcessDataMethod: string;
|
||||
batchCancelProcessDataMethod: string;
|
||||
|
||||
confirmProcessTransactionMethod: string;
|
||||
batchConfirmProcessTransactionMethod: string;
|
||||
cancelProcessTransactionMethod: string;
|
||||
batchCancelProcessTransactionMethod: string;
|
||||
rollbackProcessTransactionMethod: string;
|
||||
batchRollbackProcessTransactionMethod: string;
|
||||
holdProcessTransactionMethod: string;
|
||||
batchHoldProcessTransactionMethod: string;
|
||||
}
|
||||
|
||||
// ─── Request Descriptor ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Describes a single operation in terms of its URL, method, and
|
||||
* action header. Used by the generic `execute()` method.
|
||||
*/
|
||||
export interface RequestDescriptor {
|
||||
/** Key into the ApiURLMap. */
|
||||
urlKey: keyof ApiURLMap;
|
||||
/** Key into the RequestMethodMap. */
|
||||
methodKey: keyof RequestMethodMap;
|
||||
/** Value for the 'ex-module-action' header. */
|
||||
action: string;
|
||||
}
|
||||
|
||||
// ─── Execute Options ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Options passed to the generic `execute()` method.
|
||||
*/
|
||||
export interface ExecuteOptions {
|
||||
/** Dynamic URL parameters (e.g., `{ id: '42' }`). */
|
||||
variableURL?: Record<string, string>;
|
||||
/** Additional Axios request config (params, data, headers, etc). */
|
||||
config?: AxiosRequestConfig;
|
||||
/** Per-request telemetry context for custom spans, tags, events. */
|
||||
telemetryContext?: TelemetryContext;
|
||||
}
|
||||
|
||||
// ─── Data Services Constructor ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Configuration for constructing a BaseRemoteDataServices instance.
|
||||
*/
|
||||
export interface DataServicesConfig {
|
||||
/** Base API path (e.g., '/bookings'). Used to generate all URL templates. */
|
||||
apiUrl?: string;
|
||||
/** Module key for the 'ex-module-key' header (e.g., 'BOOKING'). */
|
||||
moduleKey?: string;
|
||||
/** Override specific URL templates. */
|
||||
urls?: Partial<ApiURLMap>;
|
||||
/** Override specific HTTP methods. */
|
||||
methods?: Partial<RequestMethodMap>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Interpolates dynamic URL parameters.
|
||||
*
|
||||
* Replaces `:paramName` segments in a URL template with values
|
||||
* from the provided variables object.
|
||||
*
|
||||
* @param template - URL template (e.g., '/bookings/:id/confirm')
|
||||
* @param variables - Key-value map of parameter names to values
|
||||
* @returns The interpolated URL string
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* interpolateUrl('/bookings/:id/confirm', { id: '42' });
|
||||
* // => '/bookings/42/confirm'
|
||||
*
|
||||
* interpolateUrl('/orgs/:orgId/users/:userId', { orgId: 'a', userId: 'b' });
|
||||
* // => '/orgs/a/users/b'
|
||||
* ```
|
||||
*/
|
||||
export function interpolateUrl(
|
||||
template: string,
|
||||
variables?: Record<string, string>,
|
||||
): string {
|
||||
if (!variables || Object.keys(variables).length === 0) {
|
||||
return template;
|
||||
}
|
||||
|
||||
return template
|
||||
.split('/')
|
||||
.map((segment) => {
|
||||
if (segment.startsWith(':')) {
|
||||
const key = segment.slice(1);
|
||||
const value = variables[key];
|
||||
if (value === undefined) {
|
||||
throw new Error(
|
||||
`[interpolateUrl] Missing value for URL parameter ":${key}" in template "${template}"`,
|
||||
);
|
||||
}
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
return segment;
|
||||
})
|
||||
.join('/');
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { AxiosError, type AxiosResponse } from 'axios';
|
||||
import { ApiErrorCode, httpStatusToErrorCode } from './error-codes';
|
||||
|
||||
/**
|
||||
* Structured API error that normalizes Axios errors into a
|
||||
* predictable, serializable format.
|
||||
*
|
||||
* Replaces the legacy `ErrorRequest` class with richer metadata.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* try {
|
||||
* await apiClient.get('/users');
|
||||
* } catch (err) {
|
||||
* if (err instanceof ApiError) {
|
||||
* console.log(err.code); // ApiErrorCode.UNAUTHORIZED
|
||||
* console.log(err.status); // 401
|
||||
* console.log(err.data); // { message: "Token expired" }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
/** Structured error code for programmatic handling. */
|
||||
readonly code: ApiErrorCode;
|
||||
|
||||
/** HTTP status code (0 if no response, e.g., network error). */
|
||||
readonly status: number;
|
||||
|
||||
/** Raw response body from the server, if available. */
|
||||
readonly data: unknown;
|
||||
|
||||
/** The original Axios error, preserved for debugging. */
|
||||
readonly cause: AxiosError | undefined;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
code: ApiErrorCode,
|
||||
status: number,
|
||||
data?: unknown,
|
||||
cause?: AxiosError,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.code = code;
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
this.cause = cause;
|
||||
|
||||
// Maintain proper prototype chain for instanceof checks
|
||||
Object.setPrototypeOf(this, ApiError.prototype);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: creates an ApiError from an AxiosError.
|
||||
* Automatically resolves the error code from the HTTP status.
|
||||
*/
|
||||
static fromAxiosError(error: AxiosError<unknown>): ApiError {
|
||||
// Network error (no response received)
|
||||
if (!error.response) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return new ApiError(
|
||||
'Request timed out',
|
||||
ApiErrorCode.TIMEOUT,
|
||||
0,
|
||||
undefined,
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (error.code === 'ERR_CANCELED') {
|
||||
return new ApiError(
|
||||
'Request was cancelled',
|
||||
ApiErrorCode.CANCELLED,
|
||||
0,
|
||||
undefined,
|
||||
error,
|
||||
);
|
||||
}
|
||||
return new ApiError(
|
||||
error.message || 'Network error',
|
||||
ApiErrorCode.NETWORK_ERROR,
|
||||
0,
|
||||
undefined,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
// Server responded with an error status
|
||||
const response: AxiosResponse = error.response;
|
||||
const status = response.status;
|
||||
const data = response.data;
|
||||
const code = httpStatusToErrorCode(status);
|
||||
|
||||
// Extract message from common server response formats
|
||||
const serverMessage =
|
||||
(data && typeof data === 'object' && 'message' in data)
|
||||
? String((data as Record<string, unknown>).message)
|
||||
: `Request failed with status ${status}`;
|
||||
|
||||
return new ApiError(serverMessage, code, status, data, error);
|
||||
}
|
||||
|
||||
/** Convenience check for authentication failures. */
|
||||
get isUnauthorized(): boolean {
|
||||
return this.code === ApiErrorCode.UNAUTHORIZED;
|
||||
}
|
||||
|
||||
/** Convenience check for permission failures. */
|
||||
get isForbidden(): boolean {
|
||||
return this.code === ApiErrorCode.FORBIDDEN;
|
||||
}
|
||||
|
||||
/** Convenience check for network/connectivity issues. */
|
||||
get isNetworkError(): boolean {
|
||||
return this.code === ApiErrorCode.NETWORK_ERROR;
|
||||
}
|
||||
|
||||
/** JSON-serializable representation for logging/telemetry. */
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
code: this.code,
|
||||
status: this.status,
|
||||
data: this.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Enumeration of well-known API error codes.
|
||||
*
|
||||
* Use these to programmatically handle specific server responses
|
||||
* without relying on magic strings scattered across the codebase.
|
||||
*/
|
||||
export enum ApiErrorCode {
|
||||
// ─── HTTP Standard ────────────────────────────────────────────
|
||||
BAD_REQUEST = 'BAD_REQUEST',
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
CONFLICT = 'CONFLICT',
|
||||
UNPROCESSABLE_ENTITY = 'UNPROCESSABLE_ENTITY',
|
||||
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
|
||||
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
||||
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
||||
|
||||
// ─── Client-side ──────────────────────────────────────────────
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
TIMEOUT = 'TIMEOUT',
|
||||
CANCELLED = 'CANCELLED',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps HTTP status codes to ApiErrorCode enum values.
|
||||
*/
|
||||
export function httpStatusToErrorCode(status: number): ApiErrorCode {
|
||||
switch (status) {
|
||||
case 400: return ApiErrorCode.BAD_REQUEST;
|
||||
case 401: return ApiErrorCode.UNAUTHORIZED;
|
||||
case 403: return ApiErrorCode.FORBIDDEN;
|
||||
case 404: return ApiErrorCode.NOT_FOUND;
|
||||
case 409: return ApiErrorCode.CONFLICT;
|
||||
case 422: return ApiErrorCode.UNPROCESSABLE_ENTITY;
|
||||
case 429: return ApiErrorCode.TOO_MANY_REQUESTS;
|
||||
case 500: return ApiErrorCode.INTERNAL_SERVER_ERROR;
|
||||
case 503: return ApiErrorCode.SERVICE_UNAVAILABLE;
|
||||
default: return ApiErrorCode.UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ApiError } from './api-error';
|
||||
export { ApiErrorCode, httpStatusToErrorCode } from './error-codes';
|
||||
@@ -0,0 +1,116 @@
|
||||
import axios, { type AxiosInstance, type AxiosError } from 'axios';
|
||||
import type { HttpClientConfig, InterceptorHooks } from './types';
|
||||
import { noopObservabilityAdapter } from '../observability/noop.adapter';
|
||||
import { ApiError } from '../errors/api-error';
|
||||
|
||||
/**
|
||||
* Creates an isolated Axios instance with per-app configuration.
|
||||
*
|
||||
* **CRITICAL**: This function creates a NEW AxiosInstance on every call.
|
||||
* It NEVER touches `axios.defaults` or `axios.interceptors`. Each consumer
|
||||
* receives a fully autonomous client with its own interceptor chain.
|
||||
*
|
||||
* @param config - Base configuration (URL, timeout, headers, observability).
|
||||
* @param hooks - Optional per-app interceptor hooks for auth, error handling, etc.
|
||||
* @returns A configured, isolated AxiosInstance.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // apps/web — full auth + telemetry
|
||||
* const apiClient = createHttpClient(
|
||||
* { baseURL: 'https://api.eigen.co/v1', observability: otelAdapter },
|
||||
* {
|
||||
* onRequest: async (config) => {
|
||||
* config.headers.Authorization = `Bearer ${getToken()}`;
|
||||
* return config;
|
||||
* },
|
||||
* onResponseError: async (error) => {
|
||||
* if (error.response?.status === 401) redirect('/login');
|
||||
* throw error;
|
||||
* },
|
||||
* },
|
||||
* );
|
||||
*
|
||||
* // apps/landing — minimal public client
|
||||
* const publicClient = createHttpClient({
|
||||
* baseURL: 'https://api.eigen.co/public/v1',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createHttpClient(
|
||||
config: HttpClientConfig,
|
||||
hooks?: InterceptorHooks,
|
||||
): AxiosInstance {
|
||||
const observability = config.observability ?? noopObservabilityAdapter;
|
||||
|
||||
// ── Create isolated instance ──────────────────────────────────
|
||||
const instance = axios.create({
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.timeout ?? 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
...(config.defaultHeaders ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
// ── Request Interceptor Chain ─────────────────────────────────
|
||||
instance.interceptors.request.use(
|
||||
async (reqConfig) => {
|
||||
// 1. Observability hook (tracing span start)
|
||||
// Wrapped in try-catch: adapter failures must never block the request
|
||||
try {
|
||||
observability.onRequestStart(reqConfig);
|
||||
} catch (adapterError) {
|
||||
console.warn('[core-api] Observability adapter error in onRequestStart:', adapterError);
|
||||
}
|
||||
|
||||
// 2. App-specific hook (e.g., inject auth token)
|
||||
if (hooks?.onRequest) {
|
||||
return hooks.onRequest(reqConfig);
|
||||
}
|
||||
|
||||
return reqConfig;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
// ── Response Interceptor Chain ────────────────────────────────
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
// 1. Observability hook (tracing span end)
|
||||
try {
|
||||
observability.onRequestEnd(response);
|
||||
} catch (adapterError) {
|
||||
console.warn('[core-api] Observability adapter error in onRequestEnd:', adapterError);
|
||||
}
|
||||
|
||||
// 2. App-specific response transform
|
||||
if (hooks?.onResponse) {
|
||||
return hooks.onResponse(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
// 1. Observability hook (tracing error record)
|
||||
// CRITICAL: Wrapped in try-catch so adapter crashes never
|
||||
// swallow the original API error from the UI.
|
||||
try {
|
||||
observability.onRequestError(error);
|
||||
} catch (adapterError) {
|
||||
console.warn('[core-api] Observability adapter error in onRequestError:', adapterError);
|
||||
}
|
||||
|
||||
// 2. App-specific error handler (e.g., 401 redirect)
|
||||
if (hooks?.onResponseError) {
|
||||
return hooks.onResponseError(error);
|
||||
}
|
||||
|
||||
// 3. Default: wrap in structured ApiError
|
||||
throw ApiError.fromAxiosError(error);
|
||||
},
|
||||
);
|
||||
|
||||
return instance;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export { createHttpClient } from './create-http-client';
|
||||
export type {
|
||||
HttpClientConfig,
|
||||
InterceptorHooks,
|
||||
ApiResponse,
|
||||
TelemetryContext,
|
||||
AxiosInstance,
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
AxiosRequestConfig,
|
||||
InternalAxiosRequestConfig,
|
||||
} from './types';
|
||||
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
import type { IObservabilityAdapter } from '../observability/types';
|
||||
|
||||
// ─── Factory Configuration ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Configuration for creating an isolated HTTP client instance.
|
||||
* Each app provides its own config — no globals are shared.
|
||||
*/
|
||||
export interface HttpClientConfig {
|
||||
/** Base URL for all requests (e.g., 'https://api.eigen.co/v1'). */
|
||||
baseURL: string;
|
||||
|
||||
/** Default request timeout in milliseconds. @default 15000 */
|
||||
timeout?: number;
|
||||
|
||||
/** Default headers applied to every outgoing request. */
|
||||
defaultHeaders?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Observability adapter for tracing, logging, and metrics.
|
||||
* If not provided, a zero-overhead No-Op adapter is used.
|
||||
*/
|
||||
observability?: IObservabilityAdapter;
|
||||
}
|
||||
|
||||
// ─── Interceptor Hooks ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Per-app hooks for customizing request/response behavior.
|
||||
*
|
||||
* These hooks are the app's "autonomy layer" — each app decides
|
||||
* how to inject tokens, handle 401s, transform responses, etc.
|
||||
*/
|
||||
export interface InterceptorHooks {
|
||||
/**
|
||||
* Called before every request is dispatched.
|
||||
* Use this to inject authentication tokens, tenant headers, etc.
|
||||
*/
|
||||
onRequest?: (
|
||||
config: InternalAxiosRequestConfig,
|
||||
) => Promise<InternalAxiosRequestConfig> | InternalAxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* Called on every successful response (2xx status).
|
||||
* Use this to normalize response shapes if needed.
|
||||
*/
|
||||
onResponse?: (response: AxiosResponse) => AxiosResponse;
|
||||
|
||||
/**
|
||||
* Called on every failed response (non-2xx or network error).
|
||||
* Use this for app-specific error handling (e.g., redirect on 401).
|
||||
* MUST throw or return a rejected promise.
|
||||
*/
|
||||
onResponseError?: (error: AxiosError) => Promise<never>;
|
||||
}
|
||||
|
||||
// ─── Standardized API Response ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Type-safe API response wrapper.
|
||||
*
|
||||
* Replaces the legacy `ResponseEntity` and the lost-in-callback
|
||||
* `Promise<void>` return type with a fully typed contract.
|
||||
*/
|
||||
export interface ApiResponse<T = unknown> {
|
||||
/** The parsed response body. */
|
||||
data: T;
|
||||
|
||||
/** The HTTP status code. */
|
||||
status: number;
|
||||
}
|
||||
|
||||
// ─── Per-Request Telemetry Context ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Advanced escape hatch for per-request telemetry enrichment.
|
||||
*
|
||||
* Attach this to any request to push custom spans, tags, or
|
||||
* business events into the observability pipeline.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await bookingServices.getMany({
|
||||
* telemetryContext: {
|
||||
* customSpanName: 'booking.list.fetch',
|
||||
* tags: { region: 'asia', priority: 'high' },
|
||||
* pushEventOnSuccess: 'booking_list_loaded',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export interface TelemetryContext {
|
||||
/** Custom keys/tags to enrich the Faro log/error or OTel Span. */
|
||||
tags?: Record<string, string | number | boolean>;
|
||||
/** If provided, manually starts a custom OTel span wrapping this request. */
|
||||
customSpanName?: string;
|
||||
/** Force an explicit business event to be pushed to Faro on success. */
|
||||
pushEventOnSuccess?: string;
|
||||
}
|
||||
|
||||
// ─── Re-export Axios types consumers frequently need ────────────
|
||||
|
||||
export type {
|
||||
AxiosInstance,
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
AxiosRequestConfig,
|
||||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
|
||||
// ─── Augment Axios to carry TelemetryContext ────────────────────
|
||||
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
/** Per-request telemetry context for custom spans, tags, events. */
|
||||
telemetryContext?: TelemetryContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type { IObservabilityAdapter } from './types';
|
||||
export { noopObservabilityAdapter } from './noop.adapter';
|
||||
export { faroAdapter } from './otel.adapter';
|
||||
export { initTelemetry, getFaro } from './setup';
|
||||
export type { TelemetryConfig } from './setup';
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { IObservabilityAdapter } from './types';
|
||||
|
||||
/**
|
||||
* No-Op Observability Adapter.
|
||||
*
|
||||
* Default adapter when no telemetry is configured.
|
||||
* All methods are empty — V8's TurboFan JIT compiler will inline
|
||||
* and dead-code-eliminate these calls during optimization,
|
||||
* resulting in effectively ZERO runtime overhead.
|
||||
*
|
||||
* Used by apps that don't need APM (e.g., `apps/landing`).
|
||||
*/
|
||||
export const noopObservabilityAdapter: IObservabilityAdapter = {
|
||||
onRequestStart() {},
|
||||
onRequestEnd() {},
|
||||
onRequestError() {},
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
import { trace, SpanStatusCode, type Span } from '@opentelemetry/api';
|
||||
import { LogLevel } from '@grafana/faro-web-sdk';
|
||||
import type { IObservabilityAdapter } from './types';
|
||||
import type { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||
import type { TelemetryContext } from '../http-client/types';
|
||||
import { getFaro } from './setup';
|
||||
|
||||
// ─── Symbol Keys ────────────────────────────────────────────────
|
||||
|
||||
/** Symbol-keyed storage for custom spans on the Axios config. */
|
||||
const CUSTOM_SPAN_KEY = Symbol('__customOtelSpan');
|
||||
|
||||
function attachSpan(config: InternalAxiosRequestConfig, span: Span): void {
|
||||
(config as unknown as Record<symbol, Span>)[CUSTOM_SPAN_KEY] = span;
|
||||
}
|
||||
|
||||
function getSpan(config: unknown): Span | undefined {
|
||||
if (!config) return undefined;
|
||||
return (config as Record<symbol, Span>)?.[CUSTOM_SPAN_KEY];
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely close a span, guarding against double-close.
|
||||
* After ending, removes the reference from the config to prevent
|
||||
* duplicate Span ID errors on potential Axios retries.
|
||||
*/
|
||||
function safeEndSpan(config: unknown, span: Span): void {
|
||||
span.end();
|
||||
// Detach from config to prevent double-close on retry
|
||||
if (config) {
|
||||
delete (config as Record<symbol, unknown>)[CUSTOM_SPAN_KEY];
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract TelemetryContext from an Axios config. */
|
||||
function getTelemetryContext(config: unknown): TelemetryContext | undefined {
|
||||
if (!config) return undefined;
|
||||
return (config as Record<string, unknown>)?.telemetryContext as TelemetryContext | undefined;
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
/** Convert TelemetryContext tags to a string record for Faro context. */
|
||||
function tagsToFaroContext(tags?: Record<string, string | number | boolean>): Record<string, string> {
|
||||
if (!tags) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(tags).map(([k, v]) => [k, String(v)]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the standardized base context used by ALL Faro pushLog/pushError calls.
|
||||
* Ensures `module.key`, `module.action`, and tags are always at the top-level
|
||||
* `context` object — making them directly queryable in LogQL (Loki).
|
||||
*/
|
||||
function buildBaseContext(
|
||||
method: string,
|
||||
url: string,
|
||||
moduleKey?: string,
|
||||
action?: string,
|
||||
tags?: Record<string, string | number | boolean>,
|
||||
): Record<string, string> {
|
||||
return {
|
||||
'http.method': method,
|
||||
'http.url': url,
|
||||
...(moduleKey ? { 'module.key': moduleKey } : {}),
|
||||
...(action ? { 'module.action': action } : {}),
|
||||
...tagsToFaroContext(tags),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Adapter ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Production-grade Observability Adapter.
|
||||
*
|
||||
* Strategy: **Opt-In Custom Spans + Faro/Loki Baseline Logging**
|
||||
*
|
||||
* `trace.getActiveSpan()` returns `undefined` inside Axios interceptors
|
||||
* due to browser XHR/Fetch lifecycle race conditions with Faro's
|
||||
* `TracingInstrumentation`. Therefore this adapter does NOT attempt
|
||||
* to enrich auto-instrumented spans.
|
||||
*
|
||||
* Instead it focuses on two responsibilities:
|
||||
*
|
||||
* 1. **Custom Span Mode** (opt-in via `telemetryContext.customSpanName`):
|
||||
* Creates an explicit OTel span, attaches business tags, and ensures
|
||||
* the span is ALWAYS closed — even on abort, timeout, or unexpected
|
||||
* errors — to prevent span leaks.
|
||||
*
|
||||
* 2. **Faro/Loki Baseline** (always):
|
||||
* Pushes rich contextual logs (`pushLog`), errors (`pushError`), and
|
||||
* success events (`pushEvent`) with standardized `baseContext` for
|
||||
* direct LogQL queryability.
|
||||
*
|
||||
* Safety guarantees:
|
||||
* - Spans are always closed via `safeEndSpan()` which detaches the
|
||||
* reference after closing, preventing double-close on Axios retries.
|
||||
* - Adapter errors are caught internally and never swallowed — the
|
||||
* original API error always propagates to the UI.
|
||||
* - `null`/`undefined` config guards prevent crashes on network timeouts
|
||||
* where `error.config` may be undefined.
|
||||
*/
|
||||
export const faroAdapter: IObservabilityAdapter = {
|
||||
onRequestStart(config: InternalAxiosRequestConfig) {
|
||||
const ctx = getTelemetryContext(config);
|
||||
const method = (config.method ?? 'UNKNOWN').toUpperCase();
|
||||
const url = config.url ?? '/';
|
||||
const moduleKey = config.headers?.['ex-module-key'] as string | undefined;
|
||||
const action = config.headers?.['ex-module-action'] as string | undefined;
|
||||
|
||||
// ── 1. Create optional custom span ──────────────────────────
|
||||
if (ctx?.customSpanName) {
|
||||
const tracer = trace.getTracer('@repo/core-api', '1.0.0');
|
||||
const span = tracer.startSpan(ctx.customSpanName, {
|
||||
attributes: {
|
||||
'http.method': method,
|
||||
'http.url': url,
|
||||
...(moduleKey ? { 'custom.module_key': moduleKey } : {}),
|
||||
...(action ? { 'custom.module_action': action } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Attach custom tags with `custom.` prefix
|
||||
if (ctx.tags) {
|
||||
for (const [key, value] of Object.entries(ctx.tags)) {
|
||||
span.setAttribute(`custom.${key}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
attachSpan(config, span);
|
||||
}
|
||||
|
||||
// ── 2. Push structured log (Loki) ───────────────────────────
|
||||
const faro = getFaro();
|
||||
if (faro) {
|
||||
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx?.tags);
|
||||
faro.api.pushLog(
|
||||
[`[core-api] ${method} ${url}`],
|
||||
{ level: LogLevel.DEBUG, context: baseContext },
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
onRequestEnd(response: AxiosResponse) {
|
||||
const ctx = getTelemetryContext(response.config);
|
||||
const moduleKey = response.config?.headers?.['ex-module-key'] as string | undefined;
|
||||
const action = response.config?.headers?.['ex-module-action'] as string | undefined;
|
||||
|
||||
// ── 1. Close custom span (OK) ───────────────────────────────
|
||||
const customSpan = getSpan(response.config);
|
||||
if (customSpan) {
|
||||
customSpan.setAttribute('http.status_code', response.status);
|
||||
customSpan.setStatus({ code: SpanStatusCode.OK });
|
||||
safeEndSpan(response.config, customSpan);
|
||||
}
|
||||
|
||||
// ── 2. Push success event (Faro) ────────────────────────────
|
||||
if (ctx?.pushEventOnSuccess) {
|
||||
const faro = getFaro();
|
||||
if (faro) {
|
||||
const method = (response.config?.method ?? 'UNKNOWN').toUpperCase();
|
||||
const url = response.config?.url ?? '/';
|
||||
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx.tags);
|
||||
faro.api.pushEvent(ctx.pushEventOnSuccess, {
|
||||
...baseContext,
|
||||
'http.status_code': String(response.status),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onRequestError(error: AxiosError) {
|
||||
const ctx = getTelemetryContext(error.config);
|
||||
const status = error.response?.status ?? 0;
|
||||
const method = (error.config?.method ?? 'UNKNOWN').toUpperCase();
|
||||
const url = error.config?.url ?? '/';
|
||||
const moduleKey = error.config?.headers?.['ex-module-key'] as string | undefined;
|
||||
const action = error.config?.headers?.['ex-module-action'] as string | undefined;
|
||||
|
||||
// Standardized context for ALL Faro calls in this handler
|
||||
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx?.tags);
|
||||
const errorContext: Record<string, string> = {
|
||||
...baseContext,
|
||||
'http.status_code': String(status),
|
||||
'error.message': error.message,
|
||||
};
|
||||
|
||||
// ── 1. Close custom span with error (if present) ────────────
|
||||
const customSpan = getSpan(error.config);
|
||||
if (customSpan) {
|
||||
customSpan.setAttribute('http.status_code', status);
|
||||
customSpan.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||
customSpan.recordException(error);
|
||||
safeEndSpan(error.config, customSpan);
|
||||
}
|
||||
|
||||
// ── 2. Push structured error + log (Faro → Loki) ────────────
|
||||
const faro = getFaro();
|
||||
if (faro) {
|
||||
faro.api.pushError(error, {
|
||||
type: 'api_error',
|
||||
context: errorContext,
|
||||
});
|
||||
|
||||
faro.api.pushLog(
|
||||
[`[core-api] ERROR ${method} ${url} → ${status}`],
|
||||
{ level: LogLevel.ERROR, context: errorContext },
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Centralized Telemetry Setup — Grafana Faro + OpenTelemetry.
|
||||
*
|
||||
* Provides a plug-and-play `initTelemetry()` function that hides
|
||||
* all Faro/OTel complexity behind a simple config interface.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // apps/web/src/main.tsx (top of file)
|
||||
* import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||
* initTelemetry({
|
||||
* appName: 'web',
|
||||
* appVersion: '1.0.0',
|
||||
* telemetryUrl: 'https://telemetry.eigen.co.id/collect',
|
||||
* environment: 'production',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import {
|
||||
getWebInstrumentations,
|
||||
initializeFaro,
|
||||
type Faro,
|
||||
} from '@grafana/faro-react';
|
||||
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
|
||||
|
||||
// ─── Configuration Interface ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Configuration for initializing the full telemetry stack.
|
||||
* Apps pass this once at startup — everything else is automatic.
|
||||
*/
|
||||
export interface TelemetryConfig {
|
||||
/** Application name for Faro + OTel resource attributes. */
|
||||
appName: string;
|
||||
/** Application version (SemVer). */
|
||||
appVersion: string;
|
||||
/** Grafana Faro collector URL (e.g., 'https://telemetry.eigen.co.id/collect'). */
|
||||
telemetryUrl: string;
|
||||
/** Deployment environment ('production', 'staging', 'development'). */
|
||||
environment: string;
|
||||
/**
|
||||
* Optional: Separate OTLP trace endpoint for direct Tempo ingestion.
|
||||
* If not provided, traces are only sent through Faro's built-in exporter.
|
||||
*/
|
||||
otlpTraceUrl?: string;
|
||||
/**
|
||||
* Optional: CORS URL patterns for W3C trace context propagation.
|
||||
* @default [/.* /]
|
||||
*/
|
||||
propagateTraceHeaderCorsUrls?: Array<string | RegExp>;
|
||||
}
|
||||
|
||||
// ─── Session Persistence ────────────────────────────────────────
|
||||
|
||||
const FARO_SESSION_KEY = 'faroSession';
|
||||
|
||||
function getStoredSession(): Record<string, unknown> | null {
|
||||
try {
|
||||
const data = localStorage.getItem(FARO_SESSION_KEY);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Singleton ──────────────────────────────────────────────────
|
||||
|
||||
let faroInstance: Faro | null = null;
|
||||
|
||||
/**
|
||||
* Initializes the Grafana Faro + OpenTelemetry observability stack.
|
||||
*
|
||||
* Call this ONCE at the top of your app's entry point, before any
|
||||
* React code, HTTP requests, or other imports execute.
|
||||
*
|
||||
* `TracingInstrumentation` internally handles:
|
||||
* - WebTracerProvider setup with resource attributes
|
||||
* - FaroMetaAttributesSpanProcessor (session/user enrichment)
|
||||
* - FaroTraceExporter (sends spans to the Faro collector)
|
||||
* - Auto-instrumentation for fetch/XHR
|
||||
* - `faro.api.initOTEL(trace, context)` bridge
|
||||
*
|
||||
* @returns The initialized Faro instance for advanced usage.
|
||||
*/
|
||||
export function initTelemetry(config: TelemetryConfig): Faro {
|
||||
if (faroInstance) return faroInstance;
|
||||
|
||||
const storedSession = getStoredSession();
|
||||
|
||||
// Build optional extra span processors
|
||||
const tracingOptions: Record<string, unknown> = {};
|
||||
|
||||
if (config.otlpTraceUrl) {
|
||||
tracingOptions.spanProcessor = new BatchSpanProcessor(
|
||||
new OTLPTraceExporter({
|
||||
url: config.otlpTraceUrl,
|
||||
headers: {},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
faroInstance = initializeFaro({
|
||||
url: config.telemetryUrl,
|
||||
app: {
|
||||
name: config.appName,
|
||||
version: config.appVersion,
|
||||
environment: config.environment,
|
||||
},
|
||||
sessionTracking: {
|
||||
enabled: true,
|
||||
persistent: true,
|
||||
session: storedSession ?? undefined,
|
||||
onSessionChange: (_oldSession, newSession) => {
|
||||
if (newSession) {
|
||||
localStorage.setItem(FARO_SESSION_KEY, JSON.stringify(newSession));
|
||||
}
|
||||
},
|
||||
},
|
||||
instrumentations: [
|
||||
...getWebInstrumentations(),
|
||||
|
||||
new TracingInstrumentation({
|
||||
...tracingOptions,
|
||||
instrumentationOptions: {
|
||||
propagateTraceHeaderCorsUrls:
|
||||
config.propagateTraceHeaderCorsUrls ?? [/.*/],
|
||||
fetchInstrumentationOptions: {
|
||||
applyCustomAttributesOnSpan(span) {
|
||||
span.setAttribute('app.synthetic_request', 'false');
|
||||
},
|
||||
},
|
||||
xhrInstrumentationOptions: {
|
||||
applyCustomAttributesOnSpan(span) {
|
||||
span.setAttribute('app.synthetic_request', 'false');
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return faroInstance;
|
||||
}
|
||||
|
||||
/** Returns the Faro instance (null if not yet initialized). */
|
||||
export function getFaro(): Faro | null {
|
||||
return faroInstance;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||
|
||||
/**
|
||||
* Interface-driven observability contract.
|
||||
*
|
||||
* The HTTP client calls these hooks at request lifecycle points.
|
||||
* Implementations decide whether to trace, log, metric, or do nothing.
|
||||
*
|
||||
* This interface is the ONLY dependency between the HTTP client and
|
||||
* any telemetry library. The core-api package NEVER imports
|
||||
* OpenTelemetry, Datadog, Sentry, or any vendor SDK directly.
|
||||
*/
|
||||
export interface IObservabilityAdapter {
|
||||
/** Called immediately before a request is dispatched. */
|
||||
onRequestStart(config: InternalAxiosRequestConfig): void;
|
||||
|
||||
/** Called when a response is successfully received. */
|
||||
onRequestEnd(response: AxiosResponse): void;
|
||||
|
||||
/** Called when a request fails (network error or error status). */
|
||||
onRequestError(error: AxiosError): void;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/library.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
}
|
||||
}
|
||||
Generated
+534
-61
@@ -96,10 +96,13 @@ importers:
|
||||
version: 5.5.4
|
||||
vite:
|
||||
specifier: ^5.1.4
|
||||
version: 5.4.17
|
||||
version: 5.4.17(@types/node@22.19.3)
|
||||
|
||||
apps/landing:
|
||||
dependencies:
|
||||
'@repo/core-api':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core-api
|
||||
'@repo/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
@@ -142,10 +145,13 @@ importers:
|
||||
version: 5.5.4
|
||||
vite:
|
||||
specifier: ^5.1.4
|
||||
version: 5.4.17
|
||||
version: 5.4.17(@types/node@22.19.3)
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
'@repo/core-api':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core-api
|
||||
'@repo/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
@@ -194,10 +200,10 @@ importers:
|
||||
version: 5.5.4
|
||||
vite:
|
||||
specifier: ^5.1.4
|
||||
version: 5.4.17
|
||||
version: 5.4.17(@types/node@22.19.3)
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17
|
||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||
|
||||
packages/configs/eslint:
|
||||
dependencies:
|
||||
@@ -236,6 +242,46 @@ importers:
|
||||
specifier: ^8.57.0
|
||||
version: 8.57.1
|
||||
|
||||
packages/core-api:
|
||||
dependencies:
|
||||
'@grafana/faro-react':
|
||||
specifier: ^2.1.0
|
||||
version: 2.7.0(react@19.2.3)
|
||||
'@grafana/faro-web-sdk':
|
||||
specifier: ^2.1.0
|
||||
version: 2.7.0
|
||||
'@grafana/faro-web-tracing':
|
||||
specifier: ^2.1.0
|
||||
version: 2.7.0
|
||||
'@opentelemetry/api':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.1
|
||||
'@opentelemetry/exporter-trace-otlp-http':
|
||||
specifier: ^0.213.0
|
||||
version: 0.213.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-web':
|
||||
specifier: ^2.2.0
|
||||
version: 2.7.1(@opentelemetry/api@1.9.1)
|
||||
axios:
|
||||
specifier: ^1.9.0
|
||||
version: 1.16.1
|
||||
devDependencies:
|
||||
'@repo/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../configs/eslint
|
||||
'@repo/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../configs/typescript
|
||||
eslint:
|
||||
specifier: ^8.57.1
|
||||
version: 8.57.1
|
||||
typescript:
|
||||
specifier: 5.5.4
|
||||
version: 5.5.4
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||
|
||||
packages/ui:
|
||||
dependencies:
|
||||
'@mantine/core':
|
||||
@@ -292,10 +338,10 @@ importers:
|
||||
version: 5.5.4
|
||||
vite:
|
||||
specifier: ^5.1.4
|
||||
version: 5.4.17
|
||||
version: 5.4.17(@types/node@22.19.3)
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17
|
||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||
|
||||
packages/utils:
|
||||
dependencies:
|
||||
@@ -323,7 +369,7 @@ importers:
|
||||
version: 5.5.4
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17
|
||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1156,6 +1202,62 @@ packages:
|
||||
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
|
||||
dev: true
|
||||
|
||||
/@grafana/faro-core@2.7.0:
|
||||
resolution: {integrity: sha512-fNOfWEouoHhZ4dw+oLO+UGoMBLgiLLQU56dp9+NgO3mGNwCYsFTQuKgKQfOe5DDmAbwpRISEyLbiMkEyY7mIJQ==}
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@grafana/faro-react@2.7.0(react@19.2.3):
|
||||
resolution: {integrity: sha512-95wmypcDjHdGe64TCivq3DKLut9q3RRANgKoSLSQhGHmXdKTwE2Ov0+UdUkVQ9uu3KsGGQJRuXkldP0YMIEMiw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-router: ^7.12.0
|
||||
react-router-dom: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-router:
|
||||
optional: true
|
||||
react-router-dom:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@grafana/faro-web-sdk': 2.7.0
|
||||
'@grafana/faro-web-tracing': 2.7.0
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 19.2.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@grafana/faro-web-sdk@2.7.0:
|
||||
resolution: {integrity: sha512-NIbFK2g634/KOcq6ej1LseZrbbRejBiIrO8beQpYX6CkyXVGrcdoWRfJdqkQ3Ys60h4aTH+xoAh2Oh0HebhOZg==}
|
||||
dependencies:
|
||||
'@grafana/faro-core': 2.7.0
|
||||
ua-parser-js: 1.0.41
|
||||
web-vitals: 5.2.0
|
||||
dev: false
|
||||
|
||||
/@grafana/faro-web-tracing@2.7.0:
|
||||
resolution: {integrity: sha512-k6eIJeLwm60FpCB+OevL+VLqnoibtCsnozs3ZCQpiUMt6G2PUIQ6/wkHfSEF46R2H+AuVB3nk9oHUKpS5o2wQQ==}
|
||||
dependencies:
|
||||
'@grafana/faro-web-sdk': 2.7.0
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/exporter-trace-otlp-http': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/instrumentation-fetch': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/instrumentation-xml-http-request': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.13.0:
|
||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -1197,7 +1299,7 @@ packages:
|
||||
magic-string: 0.27.0
|
||||
react-docgen-typescript: 2.4.0(typescript@5.5.4)
|
||||
typescript: 5.5.4
|
||||
vite: 5.4.17
|
||||
vite: 5.4.17(@types/node@22.19.3)
|
||||
dev: true
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.13:
|
||||
@@ -1443,6 +1545,279 @@ packages:
|
||||
which: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/api-logs@0.213.0:
|
||||
resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/api-logs@0.218.0:
|
||||
resolution: {integrity: sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/api@1.9.1:
|
||||
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
/@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/exporter-trace-otlp-http@0.213.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-tnRmJD39aWrE/Sp7F6AbRNAjKHToDkAqBi6i0lESpGWz3G+f4bhVAV6mgSXH2o18lrDVJXo6jf9bAywQw43wRA==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/exporter-trace-otlp-http@0.218.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/instrumentation-fetch@0.218.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-eP/Y5hDupb+6MwZSaMw4ZdsDz8YgfJbJ4Ta86BMVeOmI2EArwXcd0v1nfNIvUzXTPi7nakidwqeuUa3FwRwECg==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/instrumentation-xml-http-request@0.218.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-KzqSO62lTiqbT1ihKbbdJSt6A3TngIkacHJHi0SNUJmlpS0/Jg8Sn8kneKffQGjjpScODjZJ6LLs640zWlOGIg==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/instrumentation@0.218.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/api-logs': 0.218.0
|
||||
import-in-the-middle: 3.0.1
|
||||
require-in-the-middle: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/otlp-exporter-base@0.213.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-MegxAP1/n09Ob2dQvY5NBDVjAFkZRuKtWKxYev1R2M8hrsgXzQGkaMgoEKeUOyQ0FUyYcO29UOnYdQWmWa0PXg==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/otlp-exporter-base@0.218.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/otlp-transformer@0.213.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-RSuAlxFFPjeK4d5Y6ps8L2WhaQI6CXWllIjvo5nkAlBpmq2XdYWEBGiAbOF4nDs8CX4QblJDv5BbMUft3sEfDw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/api-logs': 0.213.0
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
protobufjs: 7.6.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/otlp-transformer@0.218.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/api-logs': 0.218.0
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-logs': 0.218.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/api-logs': 0.213.0
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/sdk-logs@0.218.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/api-logs': 0.218.0
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/sdk-metrics@2.6.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/semantic-conventions': 1.41.1
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
|
||||
dev: false
|
||||
|
||||
/@opentelemetry/semantic-conventions@1.41.1:
|
||||
resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==}
|
||||
engines: {node: '>=14'}
|
||||
dev: false
|
||||
|
||||
/@pkgjs/parseargs@0.11.0:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -1454,6 +1829,48 @@ packages:
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/aspromise@1.1.2:
|
||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/base64@1.1.2:
|
||||
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/codegen@2.0.5:
|
||||
resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/eventemitter@1.1.1:
|
||||
resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/fetch@1.1.1:
|
||||
resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==}
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
dev: false
|
||||
|
||||
/@protobufjs/float@1.0.2:
|
||||
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/inquire@1.1.2:
|
||||
resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/path@1.1.2:
|
||||
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/pool@1.1.0:
|
||||
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/utf8@1.1.1:
|
||||
resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==}
|
||||
dev: false
|
||||
|
||||
/@rolldown/pluginutils@1.0.0-beta.53:
|
||||
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
|
||||
dev: true
|
||||
@@ -2004,7 +2421,7 @@ packages:
|
||||
browser-assert: 1.2.1
|
||||
storybook: 8.6.15(prettier@3.5.3)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 5.4.17
|
||||
vite: 5.4.17(@types/node@22.19.3)
|
||||
dev: true
|
||||
|
||||
/@storybook/components@8.6.15(storybook@8.6.15):
|
||||
@@ -2146,7 +2563,7 @@ packages:
|
||||
resolve: 1.22.11
|
||||
storybook: 8.6.15(prettier@3.5.3)
|
||||
tsconfig-paths: 4.2.0
|
||||
vite: 5.4.17
|
||||
vite: 5.4.17(@types/node@22.19.3)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
@@ -2333,7 +2750,7 @@ packages:
|
||||
'@tailwindcss/node': 4.1.18
|
||||
'@tailwindcss/oxide': 4.1.18
|
||||
tailwindcss: 4.1.18
|
||||
vite: 5.4.17
|
||||
vite: 5.4.17(@types/node@22.19.3)
|
||||
|
||||
/@tootallnate/once@2.0.0:
|
||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||
@@ -3094,7 +3511,7 @@ packages:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.18.0
|
||||
vite: 5.4.17
|
||||
vite: 5.4.17(@types/node@22.19.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -3178,6 +3595,14 @@ packages:
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
dev: false
|
||||
|
||||
/acorn-import-attributes@1.9.5(acorn@8.15.0):
|
||||
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
|
||||
peerDependencies:
|
||||
acorn: ^8
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
dev: false
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.14.1):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -3209,7 +3634,6 @@ packages:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
@@ -3546,7 +3970,6 @@ packages:
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: true
|
||||
|
||||
/at-least-node@1.0.0:
|
||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||
@@ -3564,6 +3987,18 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/axios@1.16.1:
|
||||
resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==}
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.5
|
||||
https-proxy-agent: 5.0.1
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3894,6 +4329,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/cjs-module-lexer@2.2.0:
|
||||
resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
|
||||
dev: false
|
||||
|
||||
/clean-regexp@1.0.0:
|
||||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3986,7 +4425,6 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: true
|
||||
|
||||
/commander@5.1.0:
|
||||
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
||||
@@ -4249,7 +4687,6 @@ packages:
|
||||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/delegates@1.0.0:
|
||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
@@ -5385,6 +5822,16 @@ packages:
|
||||
/flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
/follow-redirects@1.16.0:
|
||||
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/for-each@0.3.5:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5407,7 +5854,6 @@ packages:
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.18
|
||||
dev: true
|
||||
|
||||
/fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
@@ -5723,6 +6169,12 @@ packages:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
/hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
dev: false
|
||||
|
||||
/hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
dev: false
|
||||
@@ -5782,7 +6234,6 @@ packages:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
@@ -5844,6 +6295,16 @@ packages:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
/import-in-the-middle@3.0.1:
|
||||
resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn-import-attributes: 1.9.5(acorn@8.15.0)
|
||||
cjs-module-lexer: 2.2.0
|
||||
module-details-from-path: 1.0.4
|
||||
dev: false
|
||||
|
||||
/import-meta-resolve@4.2.0:
|
||||
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
|
||||
dev: false
|
||||
@@ -6540,6 +7001,10 @@ packages:
|
||||
is-unicode-supported: 0.1.0
|
||||
dev: true
|
||||
|
||||
/long@5.3.2:
|
||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||
dev: false
|
||||
|
||||
/longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
dev: false
|
||||
@@ -7021,7 +7486,6 @@ packages:
|
||||
/mime-db@1.33.0:
|
||||
resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
@@ -7033,7 +7497,6 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.33.0
|
||||
dev: true
|
||||
|
||||
/mime@2.6.0:
|
||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||
@@ -7165,6 +7628,10 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/module-details-from-path@1.0.4:
|
||||
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
||||
dev: false
|
||||
|
||||
/mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -7712,6 +8179,30 @@ packages:
|
||||
react-is: 16.13.1
|
||||
dev: false
|
||||
|
||||
/protobufjs@7.6.1:
|
||||
resolution: {integrity: sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
'@protobufjs/base64': 1.1.2
|
||||
'@protobufjs/codegen': 2.0.5
|
||||
'@protobufjs/eventemitter': 1.1.1
|
||||
'@protobufjs/fetch': 1.1.1
|
||||
'@protobufjs/float': 1.0.2
|
||||
'@protobufjs/inquire': 1.1.2
|
||||
'@protobufjs/path': 1.1.2
|
||||
'@protobufjs/pool': 1.1.0
|
||||
'@protobufjs/utf8': 1.1.1
|
||||
'@types/node': 22.19.3
|
||||
long: 5.3.2
|
||||
dev: false
|
||||
|
||||
/proxy-from-env@2.1.0:
|
||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/pump@3.0.4:
|
||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||
dependencies:
|
||||
@@ -8059,6 +8550,16 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/require-in-the-middle@8.0.1:
|
||||
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
||||
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
module-details-from-path: 1.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/requireindex@1.2.0:
|
||||
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
|
||||
engines: {node: '>=0.10.5'}
|
||||
@@ -9111,6 +9612,11 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
/ua-parser-js@1.0.41:
|
||||
resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/unbox-primitive@1.1.0:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -9457,43 +9963,6 @@ packages:
|
||||
vfile-message: 4.0.3
|
||||
dev: false
|
||||
|
||||
/vite@5.4.17:
|
||||
resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==}
|
||||
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
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.3
|
||||
rollup: 4.39.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
/vite@5.4.17(@types/node@22.19.3):
|
||||
resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -9527,11 +9996,10 @@ packages:
|
||||
dependencies:
|
||||
'@types/node': 22.19.3
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.6
|
||||
rollup: 4.55.1
|
||||
postcss: 8.5.3
|
||||
rollup: 4.39.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vite@7.3.1:
|
||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||
@@ -9583,7 +10051,7 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vitest@4.0.17:
|
||||
/vitest@4.0.17(@opentelemetry/api@1.9.1):
|
||||
resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
@@ -9617,6 +10085,7 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@vitest/expect': 4.0.17
|
||||
'@vitest/mocker': 4.0.17(vite@7.3.1)
|
||||
'@vitest/pretty-format': 4.0.17
|
||||
@@ -9661,6 +10130,10 @@ packages:
|
||||
defaults: 1.0.4
|
||||
dev: true
|
||||
|
||||
/web-vitals@5.2.0:
|
||||
resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
|
||||
dev: false
|
||||
|
||||
/webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
dev: true
|
||||
|
||||
Reference in New Issue
Block a user