Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,73 @@ PropChain-FrontEnd/

---

## 📝 Logging

All application code MUST log through the **canonical logger** at
`@/utils/logger`:

```ts
import { logger } from '@/utils/logger';

logger.debug('…');
logger.info('…');
logger.warn('…');
logger.error('…', errorObject);
```

### Why a single import path?

- One canonical implementation owns redaction, correlation IDs, JSON output,
environment-aware levels, and singleton config (`configureLogger`).
- The legacy `@/utils/structuredLogger` module is kept as a thin
backwards-compat wrapper (re-exports + a domain-specific `StructuredLogger`
class with batching/remote delivery). It is marked **`@deprecated`** and an
ESLint `no-restricted-imports` rule blocks new imports outside the wrapper
itself. New code MUST NOT import from it.
- Direct `console.*` calls are blocked by ESLint for everything except
`src/utils/earlyErrorSuppression.ts`, which intentionally operates on the
raw global `console` because it runs **before** `logger` is initialised to
silence noisy browser-extension errors.

### Backwards compatibility

`@/utils/structuredLogger` re-exports `logger`, `createLogger`, `LogLevel`,
etc. from the canonical module so existing call sites continue to work
without changes. The wrapper itself (`StructuredLogger`, `logNetworkRequest`,
`logWeb3Activity`, `logTransaction`) is preserved for callers that rely on
its batching/remote-send semantics.

---

## 📊 Build stats plugin

`next.config.ts` includes a small `BuildStatsPlugin` that writes a JSON
snapshot of webpack output to `.next/build-stats.json` for local inspection.

To keep production builds lean and quiet, the plugin is gated by **two**
conditions:

| Condition | Value |
|-------------------------|------------------------------------------------|
| `process.env.ANALYZE` | MUST be set to `'true'` |
| `process.env.NODE_ENV` | MUST NOT be `production` |
| Server-side build? | Plugin is client-only — skipped on `isServer` |

In other words:

```bash
# Quiet (default for `next build` in production)
npm run build

# Opt-in to the JSON build-stats snapshot — local dev only
ANALYZE=true npm run dev # or: ANALYZE=true next build
```

Production CI MUST NOT pass `ANALYZE=true`; if it does the plugin is still
disabled by the `NODE_ENV === 'production'` guard.

---

## 📄 License

This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for complete details.
Expand Down
41 changes: 41 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,46 @@ export default [{
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
// Enforce the single canonical logger import path.
// structuredLogger is a backwards-compat wrapper; new code MUST import
// from '@/utils/logger' instead. See README § "Logging".
"no-restricted-imports": ["error", {
patterns: [{
group: [
"@/utils/structuredLogger",
"./structuredLogger",
"../utils/structuredLogger",
"../../utils/structuredLogger",
],
message: "Import from '@/utils/logger' instead. '@/utils/structuredLogger' is a thin backwards-compat wrapper and is deprecated.",
}],
}],
},
}, {
// earlyErrorSuppression.ts runs BEFORE logger.ts is loaded and must
// intercept raw console output. Exempt it from no-console.
files: ["src/utils/earlyErrorSuppression.ts"],
rules: {
"no-console": "off",
},
}, {
// Apply `no-console` to everything else so future direct console.* calls
// are caught at lint time.
files: ["src/**/*.{ts,tsx}"],
ignores: [
// earlyErrorSuppression.ts intentionally uses raw console; logger.ts
// and the deprecated structuredLogger.ts wrap it.
"src/utils/earlyErrorSuppression.ts",
"src/utils/logger.ts",
"src/utils/structuredLogger.ts",
// Test files and stories legitimately use console.* for debug output
// and assertions.
"src/**/__tests__/**",
"src/**/*.test.{ts,tsx}",
"src/**/*.stories.{ts,tsx}",
],
rules: {
// disallow all console.* (no `allow` options provided).
"no-console": "error",
},
}, ...storybook.configs["flat/recommended"]];
12 changes: 11 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import type { NextConfig } from "next";

const isAnalyzeEnabled = process.env.ANALYZE === "true";
const isDev = process.env.NODE_ENV === "development";
const isProd = process.env.NODE_ENV === "production";

// `BuildStatsPlugin` writes a JSON payload into `.next/` for on-demand
// inspection. It is ONLY meant for local development/debugging — production
// builds must never emit it.
// - Gate on the explicit `ANALYZE=true` opt-in flag.
// - Hard-disable on production builds even if `ANALYZE=true` is set
// (e.g. misconfigured CI).
// - Skip on server builds (this plugin is client-side only).
// See README § "Build stats plugin" for details.

const cspReportOnly = [
"default-src 'self'",
Expand Down Expand Up @@ -140,7 +150,7 @@ const nextConfig: NextConfig = {
};
}

if (isAnalyzeEnabled && !isServer) {
if (isAnalyzeEnabled && !isServer && !isProd) {
class BuildStatsPlugin {
apply(compiler: any) {
compiler.hooks.done.tap("BuildStatsPlugin", (stats: any) => {
Expand Down
5 changes: 2 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { ChainAwareProvider } from "@/providers/ChainAwareProvider";
import { useWalletPersistence } from "@/utils/walletPersistence";
import { setupExtensionErrorHandling } from "@/utils/extensionDetection";
import { structuredLogger } from "@/utils/structuredLogger";
import { errorMonitoring } from "@/utils/errorMonitoringService";
import { ErrorCategory, ErrorSeverity } from "@/types/errors";
import { logger } from "@/utils/logger";
Expand Down Expand Up @@ -36,10 +35,10 @@ function HomeContent() {
setupExtensionErrorHandling();

// Initialize structured logging and error monitoring
structuredLogger.info('Application initialized', {
logger.info('Application initialized', {
component: 'HomeContent',
action: 'initialization',
metadata: { timestamp: new Date().toISOString() },
timestamp: new Date().toISOString(),
});

// Set up global error handling
Expand Down
6 changes: 4 additions & 2 deletions src/components/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Globe } from 'lucide-react';
import { structuredLogger } from '@/utils/structuredLogger';
import { logger } from '@/utils/logger';

const languages = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
Expand Down Expand Up @@ -40,7 +40,9 @@ export function LanguageSwitcher() {
html.dir = 'ltr';
}

structuredLogger.component('LanguageSwitcher', 'changeLanguage', {
logger.info('Component: LanguageSwitcher - changeLanguage', {
component: 'LanguageSwitcher',
action: 'changeLanguage',
metadata: { languageCode, rtl: ['ar', 'he'].includes(languageCode) },
});
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/TransactionDetailsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { format } from 'date-fns';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import { toast } from 'sonner';
import { logger } from '@/utils/logger';

interface TransactionDetailsModalProps {
transaction: Transaction | null;
Expand Down Expand Up @@ -105,7 +106,7 @@ export const TransactionDetailsModal: React.FC<TransactionDetailsModalProps> = (
doc.save(`transaction-receipt-${transaction.hash.slice(0, 8)}.pdf`);
toast.success('Transaction receipt downloaded successfully');
} catch (error) {
console.error('Error downloading PDF:', error);
logger.error('Error downloading PDF', error);
toast.error('Failed to download transaction receipt');
}
};
Expand Down
15 changes: 6 additions & 9 deletions src/components/ViewToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { logger } from '@/utils/logger';

/**
* UI-only view mode for listing screens.
Expand Down Expand Up @@ -31,8 +32,7 @@ export function useViewMode() {
if (isValidViewMode(stored)) return stored;
} catch (err) {
// Swallow storage errors — fallback to default
// eslint-disable-next-line no-console
console.warn("useViewMode: localStorage unavailable, falling back to default view mode", err);
logger.warn("useViewMode: localStorage unavailable, falling back to default view mode", err);
}

return "grid";
Expand All @@ -41,8 +41,7 @@ export function useViewMode() {
// Wrap setter to validate input before persisting
const setMode = (v: ViewMode) => {
if (!isValidViewMode(v)) {
// eslint-disable-next-line no-console
console.warn("useViewMode.setMode called with invalid mode:", v);
logger.warn("useViewMode.setMode called with invalid mode:", v);
return;
}
setModeRaw(v);
Expand All @@ -53,8 +52,7 @@ export function useViewMode() {
localStorage.setItem(STORAGE_KEY, mode);
} catch (err) {
// Storage might be disabled; log and continue without throwing
// eslint-disable-next-line no-console
console.warn("useViewMode: failed to persist mode to localStorage", err);
logger.warn("useViewMode: failed to persist mode to localStorage", err);
}
}, [mode]);

Expand All @@ -80,11 +78,10 @@ export function ViewToggle({ mode, onChange }: ViewToggleProps) {
if (!isValidViewMode(v)) return;
try {
if (typeof onChange === "function") onChange(v);
else console.warn("ViewToggle: onChange is not a function", onChange);
else logger.warn("ViewToggle: onChange is not a function", onChange);
} catch (err) {
// Avoid bubbling UI errors — log instead
// eslint-disable-next-line no-console
console.error("ViewToggle: onChange handler threw an error", err);
logger.error("ViewToggle: onChange handler threw an error", err);
}
};

Expand Down
38 changes: 15 additions & 23 deletions src/components/error/GlobalErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React, { Component, type ReactNode, type ErrorInfo } from 'react';
import { ErrorCategory, ErrorSeverity, type AppError } from '@/types/errors';
import { ErrorFactory } from '@/utils/errorFactory';
import { structuredLogger } from '@/utils/structuredLogger';
import { logger } from '@/utils/logger';
import { errorMonitoring } from '@/utils/errorMonitoringService';

interface Props {
Expand Down Expand Up @@ -58,13 +58,11 @@ export class GlobalErrorBoundary extends Component<Props, State> {
this.setState({ error: appError });

// Log structured error
structuredLogger.error('Error caught by global boundary', appError, {
logger.errorWithStack('Error caught by global boundary', appError, {
component: 'GlobalErrorBoundary',
action: 'error_boundary_catch',
metadata: {
errorId: appError.id,
componentStack: errorInfo.componentStack,
},
errorId: appError.id,
componentStack: errorInfo.componentStack,
});

// Monitor error
Expand All @@ -78,14 +76,12 @@ export class GlobalErrorBoundary extends Component<Props, State> {

handleRetry = async (): Promise<void> => {
if (this.state.retryCount >= this.maxRetries) {
structuredLogger.warn('Max retry attempts reached', {
logger.warn('Max retry attempts reached', {
component: 'GlobalErrorBoundary',
action: 'retry_limit_reached',
metadata: {
errorId: this.state.errorId,
retryCount: this.state.retryCount,
maxRetries: this.maxRetries,
},
errorId: this.state.errorId,
retryCount: this.state.retryCount,
maxRetries: this.maxRetries,
});
return;
}
Expand All @@ -102,13 +98,11 @@ export class GlobalErrorBoundary extends Component<Props, State> {
const recovered = await errorMonitoring.attemptRecovery(this.state.error);

if (recovered) {
structuredLogger.info('Error recovery successful', {
logger.info('Error recovery successful', {
component: 'GlobalErrorBoundary',
action: 'recovery_success',
metadata: {
errorId: this.state.errorId,
retryCount: this.state.retryCount + 1,
},
errorId: this.state.errorId,
retryCount: this.state.retryCount + 1,
});
}
}
Expand All @@ -121,13 +115,11 @@ export class GlobalErrorBoundary extends Component<Props, State> {
isRecovering: false,
}));
} catch (recoveryError) {
structuredLogger.error('Error recovery failed', recoveryError as Error, {
logger.errorWithStack('Error recovery failed', recoveryError as Error, {
component: 'GlobalErrorBoundary',
action: 'recovery_failed',
metadata: {
errorId: this.state.errorId,
retryCount: this.state.retryCount + 1,
},
errorId: this.state.errorId,
retryCount: this.state.retryCount + 1,
});

this.setState({
Expand All @@ -146,7 +138,7 @@ export class GlobalErrorBoundary extends Component<Props, State> {
isRecovering: false,
});

structuredLogger.info('Error boundary reset', {
logger.info('Error boundary reset', {
component: 'GlobalErrorBoundary',
action: 'boundary_reset',
});
Expand Down
30 changes: 30 additions & 0 deletions src/store/referral/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

/**
* @/store/referral — barrel re-export for the focused slice refactor of the
* monothilic referralStore.
*
* Consumers MAY import directly from this barrel (`@/store/referral`) or
* continue to import from `@/store/referralStore` (which re-exports the same
* hooks for backwards compatibility).
*
* Migration plan:
* 1. New code SHOULD import from `@/store/referral/<slice>`.
* 2. Existing consumer imports of `@/store/referralStore` keep working
* unchanged.
* 3. Once all consumers are migrated, the `@/store/referralStore`
* re-export layer can be removed.
*/

export { useReferralLinks } from './referralLinks';
export { useReferralStats, useRecentRewards } from './referralStats';
export { useLeaderboard, useLeaderboardCache } from './leaderboard';
export {
useReferralNotification,
useReferralLoading,
useReferralError,
} from './referralNotifications';
export {
useCurrentReferralCampaign,
useReferralTermsAccepted,
} from './misc';
Loading