@tumbaland/frontend-core 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apiClient.d.ts +3 -3
- package/dist/apiClient.js +18 -6
- package/dist/authService.js +1 -1
- package/dist/groupService.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/monitoring/batch.d.ts +2 -0
- package/dist/monitoring/batch.js +87 -0
- package/dist/monitoring/config.d.ts +16 -0
- package/dist/monitoring/config.js +53 -0
- package/dist/monitoring/console.d.ts +3 -0
- package/dist/monitoring/console.js +40 -0
- package/dist/monitoring/errors.d.ts +12 -0
- package/dist/monitoring/errors.js +67 -0
- package/dist/monitoring/index.d.ts +9 -0
- package/dist/monitoring/index.js +17 -0
- package/dist/monitoring/logger.d.ts +6 -0
- package/dist/monitoring/logger.js +18 -0
- package/dist/monitoring/performance.d.ts +24 -0
- package/dist/monitoring/performance.js +118 -0
- package/dist/monitoring/utils.d.ts +3 -0
- package/dist/monitoring/utils.js +25 -0
- package/package.json +2 -2
package/dist/apiClient.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface ApiClientOptions {
|
|
|
7
7
|
export interface ApiRequestOptions extends RequestInit {
|
|
8
8
|
/** Skip the onUnauthorized callback for this call (e.g. an auth-check that expects 401 as a normal "not logged in" result, not a hard redirect). */
|
|
9
9
|
skipAuthRedirect?: boolean;
|
|
10
|
+
/** Serialized onto the URL as a query string; undefined/null values are omitted. */
|
|
11
|
+
params?: Record<string, string | number | boolean | undefined | null>;
|
|
10
12
|
}
|
|
11
13
|
export declare class ApiError extends Error {
|
|
12
14
|
status: number;
|
|
@@ -18,9 +20,7 @@ export declare class ApiError extends Error {
|
|
|
18
20
|
* correlation/session headers, structured errors, and an opt-in 401 →
|
|
19
21
|
* redirect-to-auth hook, per TECHNICAL_REVIEW.md P1.3. `authService` and
|
|
20
22
|
* `groupService` don't route through this: they use different auth
|
|
21
|
-
* transports (cookie vs. Bearer token) that predate this client.
|
|
22
|
-
* call sites (the many per-front album/finance/etc. service files that
|
|
23
|
-
* still hand-roll fetch) can adopt this incrementally.
|
|
23
|
+
* transports (cookie vs. Bearer token) that predate this client.
|
|
24
24
|
*/
|
|
25
25
|
export declare function createApiClient(options: ApiClientOptions): {
|
|
26
26
|
request: <T = unknown>(path: string, init?: ApiRequestOptions) => Promise<T>;
|
package/dist/apiClient.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { getCorrelationId, getSessionId } from '
|
|
1
|
+
import { getCorrelationId, getSessionId } from './monitoring';
|
|
2
|
+
function appendParams(path, params) {
|
|
3
|
+
if (!params)
|
|
4
|
+
return path;
|
|
5
|
+
const searchParams = new URLSearchParams();
|
|
6
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
7
|
+
if (value !== undefined && value !== null) {
|
|
8
|
+
searchParams.append(key, String(value));
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const query = searchParams.toString();
|
|
12
|
+
if (!query)
|
|
13
|
+
return path;
|
|
14
|
+
return `${path}${path.includes('?') ? '&' : '?'}${query}`;
|
|
15
|
+
}
|
|
2
16
|
export class ApiError extends Error {
|
|
3
17
|
constructor(status, message, body) {
|
|
4
18
|
super(message);
|
|
@@ -12,14 +26,12 @@ export class ApiError extends Error {
|
|
|
12
26
|
* correlation/session headers, structured errors, and an opt-in 401 →
|
|
13
27
|
* redirect-to-auth hook, per TECHNICAL_REVIEW.md P1.3. `authService` and
|
|
14
28
|
* `groupService` don't route through this: they use different auth
|
|
15
|
-
* transports (cookie vs. Bearer token) that predate this client.
|
|
16
|
-
* call sites (the many per-front album/finance/etc. service files that
|
|
17
|
-
* still hand-roll fetch) can adopt this incrementally.
|
|
29
|
+
* transports (cookie vs. Bearer token) that predate this client.
|
|
18
30
|
*/
|
|
19
31
|
export function createApiClient(options) {
|
|
20
32
|
async function request(path, init = {}) {
|
|
21
|
-
const { skipAuthRedirect, headers, ...rest } = init;
|
|
22
|
-
const response = await fetch(`${options.baseUrl()}${path}`, {
|
|
33
|
+
const { skipAuthRedirect, headers, params, ...rest } = init;
|
|
34
|
+
const response = await fetch(`${options.baseUrl()}${appendParams(path, params)}`, {
|
|
23
35
|
credentials: 'include',
|
|
24
36
|
...rest,
|
|
25
37
|
headers: {
|
package/dist/authService.js
CHANGED
package/dist/groupService.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export type { AuthServiceConfig, AuthService } from './authService';
|
|
|
5
5
|
export { createGroupService } from './groupService';
|
|
6
6
|
export type { GroupServiceConfig, GroupService } from './groupService';
|
|
7
7
|
export type { User, AuthResponse, Group, ApiResponse } from './types';
|
|
8
|
+
export { initMonitoring, captureException, captureMessage, setUser, setTag, startTransaction, recordMetric, initWebVitals, flushLogs, initConsoleInterception, generateCorrelationId, getCorrelationId, getSessionId, logger } from './monitoring';
|
|
9
|
+
export type { MonitoringConfig } from './monitoring';
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
export { createApiClient, ApiError } from './apiClient';
|
|
3
3
|
export { createAuthService } from './authService';
|
|
4
4
|
export { createGroupService } from './groupService';
|
|
5
|
+
// Monitoring: headless logging, error capture, correlation IDs, and web-vitals
|
|
6
|
+
export { initMonitoring, captureException, captureMessage, setUser, setTag, startTransaction, recordMetric, initWebVitals, flushLogs, initConsoleInterception, generateCorrelationId, getCorrelationId, getSessionId, logger } from './monitoring';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Batch logging system for efficient log transmission
|
|
2
|
+
import { getMonitoringConfig } from './config';
|
|
3
|
+
// Log batching for better performance
|
|
4
|
+
let logBatch = [];
|
|
5
|
+
let batchTimeout = null;
|
|
6
|
+
const BATCH_SIZE = 10;
|
|
7
|
+
const BATCH_INTERVAL = 5000; // 5 seconds
|
|
8
|
+
// Send batched logs
|
|
9
|
+
const sendBatch = async () => {
|
|
10
|
+
if (logBatch.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const config = getMonitoringConfig();
|
|
13
|
+
if (!config.elkUrl)
|
|
14
|
+
return;
|
|
15
|
+
const batchToSend = [...logBatch];
|
|
16
|
+
logBatch = [];
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(config.elkUrl, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify(batchToSend),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Log error without triggering console interception to prevent recursion
|
|
31
|
+
const originalWarn = console.warn;
|
|
32
|
+
console.warn = () => { }; // Temporarily disable console.warn
|
|
33
|
+
try {
|
|
34
|
+
console.warn('Failed to send log batch, dropping logs:', error);
|
|
35
|
+
// Optional: log failed batch to console in development
|
|
36
|
+
if (config.isDevelopment) {
|
|
37
|
+
batchToSend.forEach(log => {
|
|
38
|
+
console.warn('📝 [DROPPED LOG]:', JSON.stringify(log));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
console.warn = originalWarn; // Restore original console.warn
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
// Queue log for batching
|
|
48
|
+
export const queueLog = (logData) => {
|
|
49
|
+
logBatch.push(logData);
|
|
50
|
+
// Send immediately if batch is full
|
|
51
|
+
if (logBatch.length >= BATCH_SIZE) {
|
|
52
|
+
if (batchTimeout) {
|
|
53
|
+
clearTimeout(batchTimeout);
|
|
54
|
+
batchTimeout = null;
|
|
55
|
+
}
|
|
56
|
+
sendBatch();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Schedule batch send
|
|
60
|
+
if (!batchTimeout) {
|
|
61
|
+
batchTimeout = setTimeout(() => {
|
|
62
|
+
batchTimeout = null;
|
|
63
|
+
sendBatch();
|
|
64
|
+
}, BATCH_INTERVAL);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// Flush logs before page unload
|
|
68
|
+
export const flushLogs = async () => {
|
|
69
|
+
if (batchTimeout) {
|
|
70
|
+
clearTimeout(batchTimeout);
|
|
71
|
+
batchTimeout = null;
|
|
72
|
+
}
|
|
73
|
+
await sendBatch();
|
|
74
|
+
};
|
|
75
|
+
// Set up page unload handler
|
|
76
|
+
if (typeof window !== 'undefined') {
|
|
77
|
+
window.addEventListener('beforeunload', () => {
|
|
78
|
+
// Synchronous flush for critical logs - use sendBeacon for reliable delivery during page unload
|
|
79
|
+
const config = getMonitoringConfig();
|
|
80
|
+
if (logBatch.length > 0 && navigator.sendBeacon && config.elkUrl) {
|
|
81
|
+
// Use sendBeacon for reliable delivery during page unload
|
|
82
|
+
const blob = new Blob([JSON.stringify(logBatch)], { type: 'application/json' });
|
|
83
|
+
navigator.sendBeacon(config.elkUrl, blob);
|
|
84
|
+
logBatch = [];
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface MonitoringConfig {
|
|
2
|
+
isDevelopment: boolean;
|
|
3
|
+
service: string;
|
|
4
|
+
component: string;
|
|
5
|
+
elkUrl?: string;
|
|
6
|
+
captureConsoleLevels?: ('debug' | 'info' | 'warn' | 'error')[];
|
|
7
|
+
}
|
|
8
|
+
export declare const isInterceptingConsole: () => boolean;
|
|
9
|
+
export declare const getMonitoringConfig: () => MonitoringConfig;
|
|
10
|
+
export declare const updateMonitoringConfig: (config: Partial<MonitoringConfig>) => void;
|
|
11
|
+
export declare const initMonitoring: (environment?: string, options?: {
|
|
12
|
+
service?: string;
|
|
13
|
+
component?: string;
|
|
14
|
+
elkUrl?: string;
|
|
15
|
+
captureConsoleLevels?: ("debug" | "info" | "warn" | "error")[];
|
|
16
|
+
}) => void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Global monitoring configuration
|
|
2
|
+
let monitoringConfig = {
|
|
3
|
+
isDevelopment: false,
|
|
4
|
+
service: 'frontend',
|
|
5
|
+
component: 'frontend'
|
|
6
|
+
};
|
|
7
|
+
// Flag to prevent recursive console interception
|
|
8
|
+
export const isInterceptingConsole = () => {
|
|
9
|
+
return typeof window !== 'undefined' && window._monitoringInterceptingConsole === true;
|
|
10
|
+
};
|
|
11
|
+
// Get current monitoring configuration
|
|
12
|
+
export const getMonitoringConfig = () => monitoringConfig;
|
|
13
|
+
// Update monitoring configuration
|
|
14
|
+
export const updateMonitoringConfig = (config) => {
|
|
15
|
+
monitoringConfig = { ...monitoringConfig, ...config };
|
|
16
|
+
};
|
|
17
|
+
// Initialize local monitoring (no remote services)
|
|
18
|
+
export const initMonitoring = (environment = 'development', options = {}) => {
|
|
19
|
+
const { service = 'frontend', component = 'frontend', elkUrl, captureConsoleLevels = ['error', 'warn'] // Default to error and warn only
|
|
20
|
+
} = options;
|
|
21
|
+
// Update global config
|
|
22
|
+
updateMonitoringConfig({
|
|
23
|
+
isDevelopment: environment === 'development',
|
|
24
|
+
service,
|
|
25
|
+
component,
|
|
26
|
+
elkUrl
|
|
27
|
+
});
|
|
28
|
+
console.log(`📊 Local monitoring initialized (${environment} mode) for ${service}/${component}`);
|
|
29
|
+
console.log(`📝 Console interception enabled for levels: ${captureConsoleLevels.join(', ')}`);
|
|
30
|
+
// Initialize console interception and error handlers after modules are loaded
|
|
31
|
+
Promise.all([
|
|
32
|
+
import('./console'),
|
|
33
|
+
import('./errors')
|
|
34
|
+
]).then(([{ initConsoleInterception }, { captureException, captureMessage }]) => {
|
|
35
|
+
// Initialize console interception with capture function
|
|
36
|
+
initConsoleInterception(captureConsoleLevels, captureMessage);
|
|
37
|
+
// Set up global error handlers
|
|
38
|
+
if (typeof window !== 'undefined') {
|
|
39
|
+
window.addEventListener('error', (event) => {
|
|
40
|
+
captureException(event.error || new Error(event.message), {
|
|
41
|
+
filename: event.filename,
|
|
42
|
+
lineno: event.lineno,
|
|
43
|
+
colno: event.colno
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
47
|
+
captureException(new Error(`Unhandled promise rejection: ${event.reason}`), {
|
|
48
|
+
type: 'unhandledrejection'
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
type CaptureMessageFn = (message: string, level: 'debug' | 'info' | 'warn' | 'error', context?: any) => void;
|
|
2
|
+
export declare const initConsoleInterception: (captureConsoleLevels?: ("debug" | "info" | "warn" | "error")[], captureMessageFn?: CaptureMessageFn) => void;
|
|
3
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Console interception for automatic log capture
|
|
2
|
+
import { isInterceptingConsole } from './config';
|
|
3
|
+
// Initialize console interception
|
|
4
|
+
export const initConsoleInterception = (captureConsoleLevels = ['error', 'warn'], captureMessageFn) => {
|
|
5
|
+
// If no capture function provided, skip interception
|
|
6
|
+
if (!captureMessageFn)
|
|
7
|
+
return;
|
|
8
|
+
// Helper function to conditionally intercept console methods
|
|
9
|
+
const interceptConsole = (methodName, level) => {
|
|
10
|
+
if (!captureConsoleLevels.includes(level))
|
|
11
|
+
return;
|
|
12
|
+
const originalMethod = console[methodName];
|
|
13
|
+
console[methodName] = (...args) => {
|
|
14
|
+
// Call original console method
|
|
15
|
+
originalMethod.apply(console, args);
|
|
16
|
+
// Prevent recursive interception by checking the global flag
|
|
17
|
+
if (isInterceptingConsole())
|
|
18
|
+
return;
|
|
19
|
+
// Send to logging API with recursion protection
|
|
20
|
+
try {
|
|
21
|
+
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
|
|
22
|
+
// Set interception flag to prevent recursion during capture
|
|
23
|
+
window._monitoringInterceptingConsole = true;
|
|
24
|
+
captureMessageFn(message, level, {
|
|
25
|
+
source: `console.${methodName}`,
|
|
26
|
+
args: args
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
window._monitoringInterceptingConsole = false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
// Intercept console methods based on configuration
|
|
35
|
+
interceptConsole('error', 'error');
|
|
36
|
+
interceptConsole('warn', 'warn');
|
|
37
|
+
interceptConsole('log', 'info');
|
|
38
|
+
interceptConsole('info', 'info');
|
|
39
|
+
interceptConsole('debug', 'debug');
|
|
40
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const captureException: (error: Error, context?: any, options?: {
|
|
2
|
+
isDevelopment?: boolean;
|
|
3
|
+
service?: string;
|
|
4
|
+
component?: string;
|
|
5
|
+
elkUrl?: string;
|
|
6
|
+
}) => Promise<void>;
|
|
7
|
+
export declare const captureMessage: (message: string, level?: "debug" | "info" | "warn" | "error", context?: any, options?: {
|
|
8
|
+
isDevelopment?: boolean;
|
|
9
|
+
service?: string;
|
|
10
|
+
component?: string;
|
|
11
|
+
elkUrl?: string;
|
|
12
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Error and message tracking
|
|
2
|
+
import { getMonitoringConfig, isInterceptingConsole } from './config';
|
|
3
|
+
import { queueLog } from './batch';
|
|
4
|
+
import { getCorrelationId, getSessionId } from './utils';
|
|
5
|
+
// Error tracking with local ELK logging
|
|
6
|
+
export const captureException = async (error, context, options = {}) => {
|
|
7
|
+
const config = { ...getMonitoringConfig(), ...options };
|
|
8
|
+
const errorData = {
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
level: 'error',
|
|
11
|
+
service: config.service,
|
|
12
|
+
component: config.component,
|
|
13
|
+
message: error.message,
|
|
14
|
+
stack: error.stack,
|
|
15
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
|
|
16
|
+
url: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
17
|
+
context: context || {},
|
|
18
|
+
correlationId: getCorrelationId(),
|
|
19
|
+
sessionId: getSessionId()
|
|
20
|
+
};
|
|
21
|
+
// Always log to console for development
|
|
22
|
+
if (config.isDevelopment) {
|
|
23
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
24
|
+
window._monitoringInterceptingConsole = true;
|
|
25
|
+
try {
|
|
26
|
+
console.error('🚨 Frontend Error:', errorData);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Send to local ELK stack if available (batched)
|
|
33
|
+
if (config.elkUrl) {
|
|
34
|
+
queueLog(errorData);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export const captureMessage = async (message, level = 'info', context, options = {}) => {
|
|
38
|
+
const config = { ...getMonitoringConfig(), ...options };
|
|
39
|
+
const logData = {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
level: level.toLowerCase(),
|
|
42
|
+
service: config.service,
|
|
43
|
+
component: config.component,
|
|
44
|
+
message,
|
|
45
|
+
url: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
46
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
|
|
47
|
+
context: context || {},
|
|
48
|
+
correlationId: getCorrelationId(),
|
|
49
|
+
sessionId: getSessionId()
|
|
50
|
+
};
|
|
51
|
+
// Always log to console for development
|
|
52
|
+
if (config.isDevelopment) {
|
|
53
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
54
|
+
window._monitoringInterceptingConsole = true;
|
|
55
|
+
try {
|
|
56
|
+
const logLevel = level.toUpperCase();
|
|
57
|
+
console.log(`[${logLevel}] ${message}`, context || '');
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Send to local ELK stack if available (batched)
|
|
64
|
+
if (config.elkUrl) {
|
|
65
|
+
queueLog(logData);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { initMonitoring } from './config';
|
|
2
|
+
export { captureException, captureMessage } from './errors';
|
|
3
|
+
export { setUser, setTag } from './performance';
|
|
4
|
+
export { startTransaction, recordMetric, initWebVitals } from './performance';
|
|
5
|
+
export { flushLogs } from './batch';
|
|
6
|
+
export { initConsoleInterception } from './console';
|
|
7
|
+
export { generateCorrelationId, getCorrelationId, getSessionId } from './utils';
|
|
8
|
+
export { logger } from './logger';
|
|
9
|
+
export type { MonitoringConfig } from './config';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Main monitoring exports - modular architecture
|
|
2
|
+
// Configuration and initialization
|
|
3
|
+
export { initMonitoring } from './config';
|
|
4
|
+
// Error and message tracking
|
|
5
|
+
export { captureException, captureMessage } from './errors';
|
|
6
|
+
// User and tag tracking
|
|
7
|
+
export { setUser, setTag } from './performance';
|
|
8
|
+
// Performance monitoring
|
|
9
|
+
export { startTransaction, recordMetric, initWebVitals } from './performance';
|
|
10
|
+
// Batch logging utilities
|
|
11
|
+
export { flushLogs } from './batch';
|
|
12
|
+
// Console interception (for advanced usage)
|
|
13
|
+
export { initConsoleInterception } from './console';
|
|
14
|
+
// Correlation ID utilities
|
|
15
|
+
export { generateCorrelationId, getCorrelationId, getSessionId } from './utils';
|
|
16
|
+
// Small ergonomic logger wrapping captureMessage/captureException
|
|
17
|
+
export { logger } from './logger';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const logger: {
|
|
2
|
+
debug: (message: string, context?: Record<string, unknown>) => Promise<void>;
|
|
3
|
+
info: (message: string, context?: Record<string, unknown>) => Promise<void>;
|
|
4
|
+
warn: (message: string, context?: Record<string, unknown>) => Promise<void>;
|
|
5
|
+
error: (message: string, error?: unknown, context?: Record<string, unknown>) => void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Small frontend logger — routes through the same captureMessage/captureException
|
|
2
|
+
// pipeline as console interception, but as an explicit call so it's captured
|
|
3
|
+
// regardless of the per-app captureConsoleLevels configuration (which usually only
|
|
4
|
+
// intercepts 'error'/'warn'), and carries structured context instead of a string blob.
|
|
5
|
+
import { captureException, captureMessage } from './errors';
|
|
6
|
+
export const logger = {
|
|
7
|
+
debug: (message, context) => captureMessage(message, 'debug', context),
|
|
8
|
+
info: (message, context) => captureMessage(message, 'info', context),
|
|
9
|
+
warn: (message, context) => captureMessage(message, 'warn', context),
|
|
10
|
+
error: (message, error, context) => {
|
|
11
|
+
if (error instanceof Error) {
|
|
12
|
+
captureException(error, { message, ...context });
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
captureMessage(message, 'error', error !== undefined ? { error, ...context } : context);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const setUser: (user: {
|
|
2
|
+
id: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
username?: string;
|
|
5
|
+
}, options?: {
|
|
6
|
+
isDevelopment?: boolean;
|
|
7
|
+
}) => void;
|
|
8
|
+
export declare const setTag: (key: string, value: string, options?: {
|
|
9
|
+
isDevelopment?: boolean;
|
|
10
|
+
}) => void;
|
|
11
|
+
export declare const startTransaction: (name: string, op: string, options?: {
|
|
12
|
+
isDevelopment?: boolean;
|
|
13
|
+
}) => {
|
|
14
|
+
finish: () => void;
|
|
15
|
+
};
|
|
16
|
+
export declare const recordMetric: (name: string, value: number | {
|
|
17
|
+
value: number;
|
|
18
|
+
id?: string;
|
|
19
|
+
}, unit?: string, options?: {
|
|
20
|
+
isDevelopment?: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
export declare const initWebVitals: (options?: {
|
|
23
|
+
isDevelopment?: boolean;
|
|
24
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Performance monitoring utilities
|
|
2
|
+
import { getMonitoringConfig, isInterceptingConsole } from './config';
|
|
3
|
+
// Web vitals will be loaded dynamically
|
|
4
|
+
let webVitals = null;
|
|
5
|
+
// User tracking (local storage only)
|
|
6
|
+
export const setUser = (user, options = {}) => {
|
|
7
|
+
const config = { ...getMonitoringConfig(), ...options };
|
|
8
|
+
if (config.isDevelopment) {
|
|
9
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
10
|
+
window._monitoringInterceptingConsole = true;
|
|
11
|
+
try {
|
|
12
|
+
console.log('👤 User set:', user);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Could store in localStorage for persistence if needed
|
|
19
|
+
};
|
|
20
|
+
export const setTag = (key, value, options = {}) => {
|
|
21
|
+
const config = { ...getMonitoringConfig(), ...options };
|
|
22
|
+
if (config.isDevelopment) {
|
|
23
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
24
|
+
window._monitoringInterceptingConsole = true;
|
|
25
|
+
try {
|
|
26
|
+
console.log(`🏷️ Tag set: ${key} = ${value}`);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Store tags locally if needed for context
|
|
33
|
+
};
|
|
34
|
+
// Performance monitoring utilities (local only)
|
|
35
|
+
export const startTransaction = (name, op, options = {}) => {
|
|
36
|
+
const config = { ...getMonitoringConfig(), ...options };
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
if (config.isDevelopment) {
|
|
39
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
40
|
+
window._monitoringInterceptingConsole = true;
|
|
41
|
+
try {
|
|
42
|
+
console.log(`⏱️ Transaction started: ${name} (${op})`);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
finish: () => {
|
|
50
|
+
const duration = Date.now() - startTime;
|
|
51
|
+
recordMetric(`${name}_duration`, duration, 'ms', { isDevelopment: config.isDevelopment });
|
|
52
|
+
if (config.isDevelopment) {
|
|
53
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
54
|
+
window._monitoringInterceptingConsole = true;
|
|
55
|
+
try {
|
|
56
|
+
console.log(`✅ Transaction finished: ${name} (${duration}ms)`);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export const recordMetric = (name, value, unit = 'ms', options = {}) => {
|
|
66
|
+
const config = { ...getMonitoringConfig(), ...options };
|
|
67
|
+
// Handle both number and metric object formats
|
|
68
|
+
const metricValue = typeof value === 'number' ? value : value.value;
|
|
69
|
+
// Always log to console for development with better formatting
|
|
70
|
+
if (config.isDevelopment) {
|
|
71
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
72
|
+
window._monitoringInterceptingConsole = true;
|
|
73
|
+
try {
|
|
74
|
+
const formattedValue = unit === 'ms' ? `${metricValue.toFixed(2)}ms` : `${metricValue}${unit}`;
|
|
75
|
+
console.log(`📊 Performance: ${name} = ${formattedValue}`);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Note: Sentry performance measurement removed due to API changes
|
|
82
|
+
// Can be re-added when Sentry API is updated
|
|
83
|
+
};
|
|
84
|
+
// Basic performance observer for web vitals
|
|
85
|
+
export const initWebVitals = async (options = {}) => {
|
|
86
|
+
const config = { ...getMonitoringConfig(), ...options };
|
|
87
|
+
if (config.isDevelopment) {
|
|
88
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
89
|
+
window._monitoringInterceptingConsole = true;
|
|
90
|
+
try {
|
|
91
|
+
console.log('📊 Web Vitals monitoring initialized (development mode)');
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
// Dynamic import of web-vitals
|
|
98
|
+
const webVitalsModule = await import('web-vitals');
|
|
99
|
+
webVitals = webVitalsModule;
|
|
100
|
+
// Use the new web-vitals API (v5+)
|
|
101
|
+
webVitals.onCLS((metric) => recordMetric('CLS', metric, 'unitless', { isDevelopment: config.isDevelopment }));
|
|
102
|
+
webVitals.onFCP((metric) => recordMetric('FCP', metric, 'ms', { isDevelopment: config.isDevelopment }));
|
|
103
|
+
webVitals.onLCP((metric) => recordMetric('LCP', metric, 'ms', { isDevelopment: config.isDevelopment }));
|
|
104
|
+
webVitals.onTTFB((metric) => recordMetric('TTFB', metric, 'ms', { isDevelopment: config.isDevelopment }));
|
|
105
|
+
// Note: FID is deprecated in web-vitals v5
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
const originalIsIntercepting = isInterceptingConsole();
|
|
109
|
+
window._monitoringInterceptingConsole = true;
|
|
110
|
+
try {
|
|
111
|
+
console.warn('Web Vitals library not available - install web-vitals package');
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
window._monitoringInterceptingConsole = originalIsIntercepting;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Utility functions for monitoring
|
|
2
|
+
// Generate correlation ID using crypto.randomUUID()
|
|
3
|
+
export const generateCorrelationId = () => {
|
|
4
|
+
return crypto.randomUUID();
|
|
5
|
+
};
|
|
6
|
+
// Get or create a session ID (persistent across the user's session)
|
|
7
|
+
let sessionId = null;
|
|
8
|
+
export const getSessionId = () => {
|
|
9
|
+
if (!sessionId) {
|
|
10
|
+
sessionId = generateCorrelationId();
|
|
11
|
+
}
|
|
12
|
+
return sessionId;
|
|
13
|
+
};
|
|
14
|
+
// Get correlation ID from server-set meta tag or generate request-specific ID
|
|
15
|
+
export const getCorrelationId = () => {
|
|
16
|
+
// Try to get from server-set meta tag first (proxy-generated)
|
|
17
|
+
if (typeof document !== 'undefined') {
|
|
18
|
+
const metaTag = document.querySelector('meta[name="x-correlation-id"]');
|
|
19
|
+
if (metaTag && metaTag.getAttribute('content')) {
|
|
20
|
+
return metaTag.getAttribute('content');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Fallback: generate per-request correlation ID
|
|
24
|
+
return generateCorrelationId();
|
|
25
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tumbaland/frontend-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Shared frontend auth/group/API-client logic for Tumbaland frontends",
|
|
5
5
|
"author": "Tumbaland",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"typecheck": "tsc --noEmit"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"
|
|
21
|
+
"web-vitals": "^5.3.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^26.1.0",
|