@jhits/plugin-telemetry 0.0.7 → 0.0.9

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/package.json CHANGED
@@ -1,36 +1,46 @@
1
1
  {
2
2
  "name": "@jhits/plugin-telemetry",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "System logging and telemetry utilities for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "main": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./dist/index.d.ts",
13
- "default": "./dist/index.js"
12
+ "types": "./src/index.ts",
13
+ "import": "./src/index.ts",
14
+ "default": "./src/index.ts"
15
+ },
16
+ "./src": {
17
+ "types": "./src/index.ts",
18
+ "import": "./src/index.ts",
19
+ "default": "./src/index.ts"
14
20
  },
15
21
  "./server": {
16
22
  "types": "./dist/server.d.ts",
23
+ "import": "./dist/server.js",
17
24
  "default": "./dist/server.js"
18
25
  },
19
26
  "./api/route": {
20
27
  "types": "./dist/api/route.d.ts",
28
+ "import": "./dist/api/route.js",
21
29
  "default": "./dist/api/route.js"
22
30
  },
23
31
  "./api/handler": {
24
32
  "types": "./dist/api/handler.d.ts",
33
+ "import": "./dist/api/handler.js",
25
34
  "default": "./dist/api/handler.js"
26
35
  },
27
36
  "./utils/logCleaner": {
28
37
  "types": "./dist/utils/logCleaner.d.ts",
38
+ "import": "./dist/utils/logCleaner.js",
29
39
  "default": "./dist/utils/logCleaner.js"
30
40
  }
31
41
  },
32
42
  "dependencies": {
33
- "@jhits/plugin-core": "0.0.2"
43
+ "@jhits/plugin-core": "0.0.7"
34
44
  },
35
45
  "peerDependencies": {
36
46
  "next": ">=15.0.0",
@@ -48,7 +58,6 @@
48
58
  },
49
59
  "files": [
50
60
  "dist",
51
- "src",
52
61
  "package.json"
53
62
  ],
54
63
  "scripts": {
@@ -1,33 +0,0 @@
1
- /**
2
- * Telemetry Provider
3
- * React context provider for telemetry functionality
4
- */
5
- "use client";
6
-
7
- import React, { createContext, useContext, ReactNode } from 'react';
8
- import { TelemetryService as ITelemetryService } from './types';
9
- import { telemetryService } from './TelemetryService';
10
-
11
- const TelemetryContext = createContext<ITelemetryService | null>(null);
12
-
13
- export interface TelemetryProviderProps {
14
- children: ReactNode;
15
- }
16
-
17
- export const TelemetryProvider: React.FC<TelemetryProviderProps> = ({ children }) => {
18
- return (
19
- <TelemetryContext.Provider value={telemetryService}>
20
- {children}
21
- </TelemetryContext.Provider>
22
- );
23
- };
24
-
25
- export const useTelemetry = (): ITelemetryService => {
26
- const context = useContext(TelemetryContext);
27
- if (!context) {
28
- // Fallback to service directly if context is not available
29
- return telemetryService;
30
- }
31
- return context;
32
- };
33
-
@@ -1,337 +0,0 @@
1
- /**
2
- * Telemetry Service
3
- * Core service for logging and managing telemetry data
4
- * Uses backend file-based logging with buffering
5
- */
6
- import { TelemetryEntry, TelemetryCategory, TelemetryService as ITelemetryService } from './types';
7
-
8
- const API_ENDPOINT = '/api/dashboard/telemetry';
9
- const BUFFER_INTERVAL_MS = 5000; // Send buffered logs every 5 seconds
10
- const MAX_BUFFER_SIZE = 50; // Max entries to buffer before forcing send
11
-
12
- class TelemetryServiceClass implements ITelemetryService {
13
- private buffer: TelemetryEntry[] = [];
14
- private flushTimer: NodeJS.Timeout | null = null;
15
- private defaultContext: string = 'system';
16
-
17
- private generateId(): string {
18
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
19
- }
20
-
21
- /**
22
- * Safe JSON stringify that handles circular references and DOM/React elements
23
- * Uses WeakSet to track seen objects and returns "[Circular]" for repeated references
24
- */
25
- private safeStringify(value: any, space?: number): string {
26
- const seen = new WeakSet();
27
- const isDOMNode = (val: any): boolean => {
28
- return (
29
- typeof Node !== 'undefined' && val instanceof Node ||
30
- typeof Element !== 'undefined' && val instanceof Element ||
31
- (val && typeof val === 'object' && 'nodeType' in val && 'nodeName' in val)
32
- );
33
- };
34
- const isReactElement = (val: any): boolean => {
35
- return (
36
- val &&
37
- typeof val === 'object' &&
38
- (val.$$typeof === Symbol.for('react.element') ||
39
- val.$$typeof === Symbol.for('react.portal') ||
40
- Object.keys(val).some(key => key.startsWith('__react') || key.startsWith('_react')))
41
- );
42
- };
43
- const isEvent = (val: any): boolean => {
44
- return (
45
- val &&
46
- typeof val === 'object' &&
47
- (val instanceof Event ||
48
- (typeof Event !== 'undefined' && val.constructor?.name === 'Event') ||
49
- (val.target && val.currentTarget && val.type))
50
- );
51
- };
52
-
53
- const replacer = (key: string, val: any): any => {
54
- // Skip React internal keys
55
- if (key.startsWith('__react') || key.startsWith('_react')) {
56
- return '[React Internal]';
57
- }
58
-
59
- // Handle null/undefined
60
- if (val === null || val === undefined) {
61
- return val;
62
- }
63
-
64
- // Handle DOM nodes
65
- if (isDOMNode(val)) {
66
- return '[DOM Node]';
67
- }
68
-
69
- // Handle React elements
70
- if (isReactElement(val)) {
71
- return '[React Element]';
72
- }
73
-
74
- // Handle Events
75
- if (isEvent(val)) {
76
- return `[Event: ${val.type || 'unknown'}]`;
77
- }
78
-
79
- // Handle functions
80
- if (typeof val === 'function') {
81
- return `[Function: ${val.name || 'anonymous'}]`;
82
- }
83
-
84
- // Handle circular references
85
- if (typeof val === 'object') {
86
- if (seen.has(val)) {
87
- return '[Circular]';
88
- }
89
- seen.add(val);
90
- }
91
-
92
- return val;
93
- };
94
-
95
- try {
96
- return JSON.stringify(value, replacer, space);
97
- } catch (error) {
98
- // Fallback if stringify still fails
99
- return JSON.stringify({
100
- error: 'Failed to stringify',
101
- message: error instanceof Error ? error.message : String(error),
102
- type: typeof value,
103
- }, null, space);
104
- }
105
- }
106
-
107
- /**
108
- * Get safe JSON serializer that handles circular references and React Fiber nodes
109
- */
110
- /**
111
- * Send entries to backend API
112
- * CRITICAL: Uses safe serializer to prevent circular JSON errors
113
- */
114
- private async sendToBackend(entries: TelemetryEntry[], useBeacon: boolean = false): Promise<void> {
115
- if (entries.length === 0) return;
116
-
117
- try {
118
- // SAFE SERIALIZER to prevent Circular structure errors
119
- const getCircularReplacer = () => {
120
- const seen = new WeakSet();
121
- return (key: string, value: any) => {
122
- if (typeof value === "object" && value !== null) {
123
- if (seen.has(value)) return "[Circular]";
124
- seen.add(value);
125
- // Strip out DOM elements and React internals
126
- if (value instanceof Node) return `[DOM: ${value.nodeName}]`;
127
- if (key.startsWith('__react')) return "[ReactInternal]";
128
- }
129
- return value;
130
- };
131
- };
132
-
133
- const data = JSON.stringify(entries, getCircularReplacer());
134
-
135
- if (navigator.sendBeacon && useBeacon) {
136
- const blob = new Blob([data], { type: 'application/json' });
137
- if (navigator.sendBeacon(API_ENDPOINT, blob)) return;
138
- }
139
-
140
- const response = await fetch(API_ENDPOINT, {
141
- method: 'POST',
142
- headers: { 'Content-Type': 'application/json' },
143
- body: data,
144
- });
145
-
146
- if (!response.ok) throw new Error(`Status ${response.status}`);
147
- } catch (error) {
148
- console.error('[TelemetryService] Serialization or Network failure:', error);
149
- }
150
- }
151
-
152
- /**
153
- * Schedule a flush of the buffer
154
- */
155
- private scheduleFlush(): void {
156
- // Clear existing timer
157
- if (this.flushTimer) {
158
- clearTimeout(this.flushTimer);
159
- }
160
-
161
- // Schedule new flush
162
- this.flushTimer = setTimeout(() => {
163
- this.flush();
164
- }, BUFFER_INTERVAL_MS);
165
- }
166
-
167
- /**
168
- * Flush buffer to backend immediately
169
- */
170
- async flush(useBeacon: boolean = false): Promise<void> {
171
- if (this.buffer.length === 0) return;
172
-
173
- const entriesToSend = [...this.buffer];
174
- this.buffer = [];
175
-
176
- if (this.flushTimer) {
177
- clearTimeout(this.flushTimer);
178
- this.flushTimer = null;
179
- }
180
-
181
- await this.sendToBackend(entriesToSend, useBeacon);
182
- }
183
-
184
- log(category: TelemetryCategory, message: string, data?: any, context?: string): void {
185
- const entry: TelemetryEntry = {
186
- id: this.generateId(),
187
- timestamp: Date.now(),
188
- category,
189
- message,
190
- data: data || {},
191
- context: context || this.defaultContext,
192
- };
193
-
194
- // Add to buffer
195
- this.buffer.push(entry);
196
-
197
- // Also log to console for immediate visibility (less intrusive, emoji-coded breadcrumbs)
198
- // Only log in development or if explicitly enabled
199
- if (process.env.NODE_ENV === 'development' || process.env.TELEMETRY_VERBOSE === 'true') {
200
- const emoji = category === 'DRAG_DROP' ? '📦' : category === 'STATE' ? '📊' : category === 'ERROR' ? '❌' : '🎨';
201
- // Use safeStringify for console logs too to prevent circular reference errors
202
- const safeData = data ? this.safeStringify(data) : '';
203
- console.log(`${emoji} [${entry.context}] ${message}`, safeData || '');
204
- }
205
-
206
- // Send immediately if ERROR, otherwise schedule flush
207
- if (category === 'ERROR') {
208
- this.flush();
209
- } else if (this.buffer.length >= MAX_BUFFER_SIZE) {
210
- // Force flush if buffer is getting large
211
- this.flush();
212
- } else {
213
- // Schedule regular flush
214
- this.scheduleFlush();
215
- }
216
- }
217
-
218
- error(message: string, error: Error | unknown, data?: any, context?: string): void {
219
- const errorObj = error instanceof Error ? error : new Error(String(error));
220
- const entry: TelemetryEntry = {
221
- id: this.generateId(),
222
- timestamp: Date.now(),
223
- category: 'ERROR',
224
- message,
225
- data: {
226
- ...data,
227
- errorMessage: errorObj.message,
228
- errorName: errorObj.name,
229
- },
230
- stack: errorObj.stack,
231
- context: context || this.defaultContext,
232
- };
233
-
234
- // Add to buffer
235
- this.buffer.push(entry);
236
-
237
- // Also log to console for immediate visibility (errors always shown, but with safe stringify)
238
- // Use safeStringify to prevent circular reference errors in console
239
- const safeData = data ? this.safeStringify(data) : '';
240
- console.error(`❌ [${entry.context}] ${message}`, error instanceof Error ? error.message : String(error), safeData || '');
241
-
242
- // Errors are sent immediately
243
- this.flush();
244
- }
245
-
246
- setContext(context: string): void {
247
- this.defaultContext = context;
248
- }
249
-
250
- exportLogs(): void {
251
- // Flush any pending logs first
252
- this.flush().then(() => {
253
- // Inform user that logs are on the server
254
- alert('Telemetry logs are stored on the server at: logs/editor-debug.log\n\nCheck the server logs directory for the file.');
255
- }).catch(() => {
256
- alert('Telemetry logs are stored on the server. Check logs/editor-debug.log on the server.');
257
- });
258
- }
259
-
260
- clearLogs(): void {
261
- // Clear the buffer (logs are already on server, can't clear server logs from client)
262
- this.buffer = [];
263
- if (this.flushTimer) {
264
- clearTimeout(this.flushTimer);
265
- this.flushTimer = null;
266
- }
267
- // Less intrusive console log
268
- if (process.env.NODE_ENV === 'development' || process.env.TELEMETRY_VERBOSE === 'true') {
269
- console.log('🧹 Telemetry buffer cleared');
270
- }
271
- }
272
-
273
- getLogs(): TelemetryEntry[] {
274
- // Return current buffer (server logs are not accessible from client)
275
- return [...this.buffer];
276
- }
277
- }
278
-
279
- // Initialize global error handlers
280
- const initializeGlobalErrorHandlers = (service: TelemetryServiceClass): void => {
281
- // Capture window.onerror
282
- const originalOnError = window.onerror;
283
- window.onerror = (message, source, lineno, colno, error) => {
284
- service.error(
285
- 'Global Error Handler',
286
- error || new Error(String(message)),
287
- {
288
- message: String(message),
289
- source: String(source),
290
- lineno,
291
- colno,
292
- }
293
- );
294
- if (originalOnError) {
295
- return originalOnError(message, source, lineno, colno, error);
296
- }
297
- return false;
298
- };
299
-
300
- // Capture unhandled promise rejections
301
- window.addEventListener('unhandledrejection', (event) => {
302
- service.error(
303
- 'Unhandled Promise Rejection',
304
- event.reason,
305
- {
306
- // Don't include the promise object itself (can cause circular references)
307
- reason: event.reason instanceof Error ? event.reason.message : String(event.reason),
308
- }
309
- );
310
- });
311
- };
312
-
313
- // Initialize unload handler for flushing logs
314
- const initializeUnloadHandler = (service: TelemetryServiceClass): void => {
315
- // Flush logs on page unload using sendBeacon
316
- window.addEventListener('beforeunload', () => {
317
- service.flush(true); // Use sendBeacon for unload
318
- });
319
-
320
- // Also handle visibility change (tab switch, minimize, etc.)
321
- document.addEventListener('visibilitychange', () => {
322
- if (document.visibilityState === 'hidden') {
323
- service.flush(true); // Use sendBeacon when tab becomes hidden
324
- }
325
- });
326
- };
327
-
328
- // Create singleton instance
329
- export const telemetryService = new TelemetryServiceClass();
330
-
331
- // Initialize global error handlers and unload handler
332
- if (typeof window !== 'undefined') {
333
- initializeGlobalErrorHandlers(telemetryService);
334
- initializeUnloadHandler(telemetryService);
335
- }
336
-
337
-
@@ -1,277 +0,0 @@
1
- import 'server-only';
2
-
3
- /**
4
- * Telemetry API Handler
5
- * Plugin-mounted API handler for telemetry logging
6
- * Compatible with Next.js API routes and Express
7
- *
8
- * IMPORTANT: This file should ONLY be imported in server-side API routes.
9
- * Do NOT import this in client-side code.
10
- */
11
- import { NextRequest, NextResponse } from 'next/server';
12
- import fs from 'fs/promises';
13
- import path from 'path';
14
-
15
- const LOG_DIR = path.join(process.cwd(), 'logs');
16
- const LOG_FILE = path.join(LOG_DIR, 'jhits-system.log');
17
- const OLD_LOG_FILE = path.join(LOG_DIR, 'jhits-system.old.log');
18
- const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
19
- const MAX_BATCH_SIZE = 100; // Maximum entries per request
20
- const MAX_ENTRY_SIZE = 100 * 1024; // 100KB per entry
21
-
22
- interface TelemetryEntry {
23
- id: string;
24
- timestamp: number;
25
- category: string;
26
- message: string;
27
- data: any;
28
- stack?: string;
29
- context?: string;
30
- userId?: string;
31
- sessionId?: string;
32
- }
33
-
34
- /**
35
- * Ensure logs directory exists
36
- */
37
- async function ensureLogDir(): Promise<void> {
38
- try {
39
- await fs.access(LOG_DIR);
40
- } catch {
41
- await fs.mkdir(LOG_DIR, { recursive: true });
42
- }
43
- }
44
-
45
- /**
46
- * Check log file size and rotate if necessary
47
- */
48
- async function rotateLogIfNeeded(): Promise<void> {
49
- try {
50
- const stats = await fs.stat(LOG_FILE);
51
- if (stats.size > MAX_LOG_SIZE) {
52
- // Rename current log to old log
53
- try {
54
- await fs.rename(LOG_FILE, OLD_LOG_FILE);
55
- } catch (error: any) {
56
- // If old log exists, remove it first
57
- if (error.code === 'EEXIST' || error.code === 'ENOENT') {
58
- try {
59
- await fs.unlink(OLD_LOG_FILE);
60
- await fs.rename(LOG_FILE, OLD_LOG_FILE);
61
- } catch {
62
- // If still fails, just overwrite
63
- await fs.copyFile(LOG_FILE, OLD_LOG_FILE);
64
- await fs.unlink(LOG_FILE);
65
- }
66
- } else {
67
- throw error;
68
- }
69
- }
70
- }
71
- } catch (error: any) {
72
- // If file doesn't exist, that's fine - we'll create it
73
- if (error.code !== 'ENOENT') {
74
- console.error('[Telemetry Handler] Error checking log size:', error);
75
- }
76
- }
77
- }
78
-
79
- /**
80
- * Format log entry as single-line JSON
81
- */
82
- function formatLogEntry(entry: TelemetryEntry): string {
83
- const timestamp = new Date(entry.timestamp).toISOString();
84
- const context = entry.context || 'unknown';
85
- const userId = entry.userId || 'anonymous';
86
- const sessionId = entry.sessionId || 'unknown';
87
-
88
- // Sanitize data to prevent log injection
89
- const sanitizedData = typeof entry.data === 'object'
90
- ? JSON.stringify(entry.data).replace(/\n/g, '\\n').replace(/\r/g, '\\r')
91
- : String(entry.data);
92
-
93
- const stackStr = entry.stack ? ` "stack":"${entry.stack.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"` : '';
94
-
95
- return `[${timestamp}] [${context}] [${userId}] [${sessionId}] [${entry.category}] ${entry.message} ${sanitizedData}${stackStr}\n`;
96
- }
97
-
98
- /**
99
- * Validate and sanitize telemetry entry
100
- */
101
- function validateEntry(entry: any): TelemetryEntry | null {
102
- // Basic validation
103
- if (!entry || typeof entry !== 'object') {
104
- return null;
105
- }
106
-
107
- // Check required fields
108
- if (!entry.id || !entry.timestamp || !entry.category || !entry.message) {
109
- return null;
110
- }
111
-
112
- // Size check to prevent log injection attacks
113
- const entrySize = JSON.stringify(entry).length;
114
- if (entrySize > MAX_ENTRY_SIZE) {
115
- console.warn('[Telemetry Handler] Entry too large, skipping:', entry.id);
116
- return null;
117
- }
118
-
119
- // Sanitize string fields
120
- return {
121
- id: String(entry.id).substring(0, 100),
122
- timestamp: Number(entry.timestamp),
123
- category: String(entry.category).substring(0, 50),
124
- message: String(entry.message).substring(0, 500),
125
- data: entry.data || {},
126
- stack: entry.stack ? String(entry.stack).substring(0, 5000) : undefined,
127
- context: entry.context ? String(entry.context).substring(0, 100) : undefined,
128
- userId: entry.userId ? String(entry.userId).substring(0, 100) : undefined,
129
- sessionId: entry.sessionId ? String(entry.sessionId).substring(0, 100) : undefined,
130
- };
131
- }
132
-
133
- /**
134
- * Extract user/session info from request headers or cookies
135
- */
136
- function extractContext(request: NextRequest): { userId?: string; sessionId?: string } {
137
- // Try to get user ID from headers (custom header)
138
- const userId = request.headers.get('x-user-id') ||
139
- request.headers.get('x-userid') ||
140
- undefined;
141
-
142
- // Try to get session ID from headers or cookies
143
- const sessionId = request.headers.get('x-session-id') ||
144
- request.headers.get('x-sessionid') ||
145
- request.cookies.get('sessionId')?.value ||
146
- request.cookies.get('session')?.value ||
147
- undefined;
148
-
149
- return { userId, sessionId };
150
- }
151
-
152
- /**
153
- * Next.js API Route Handler
154
- */
155
- export async function POST(request: NextRequest): Promise<NextResponse> {
156
- try {
157
- // Ensure log directory exists
158
- await ensureLogDir();
159
-
160
- // Rotate log if needed
161
- await rotateLogIfNeeded();
162
-
163
- // Extract context from request
164
- const { userId, sessionId } = extractContext(request);
165
-
166
- // Parse request body
167
- const body = await request.json();
168
-
169
- // Handle single entry or batch of entries
170
- const entries = Array.isArray(body) ? body : [body];
171
-
172
- // Rate limiting: Check batch size
173
- if (entries.length > MAX_BATCH_SIZE) {
174
- return NextResponse.json(
175
- { success: false, error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}` },
176
- { status: 400 }
177
- );
178
- }
179
-
180
- // Validate and sanitize entries
181
- const validEntries: TelemetryEntry[] = [];
182
- for (const entry of entries) {
183
- const validated = validateEntry(entry);
184
- if (validated) {
185
- // Add context from request if not present in entry
186
- if (!validated.userId && userId) {
187
- validated.userId = userId;
188
- }
189
- if (!validated.sessionId && sessionId) {
190
- validated.sessionId = sessionId;
191
- }
192
- validEntries.push(validated);
193
- }
194
- }
195
-
196
- if (validEntries.length === 0) {
197
- return NextResponse.json(
198
- { success: false, error: 'No valid entries to log' },
199
- { status: 400 }
200
- );
201
- }
202
-
203
- // Format and append each entry
204
- const logLines = validEntries.map(formatLogEntry).join('');
205
- await fs.appendFile(LOG_FILE, logLines, 'utf8');
206
-
207
- return NextResponse.json({
208
- success: true,
209
- logged: validEntries.length,
210
- rejected: entries.length - validEntries.length
211
- });
212
- } catch (error) {
213
- console.error('[Telemetry Handler] Error writing log:', error);
214
- return NextResponse.json(
215
- { success: false, error: 'Failed to write log' },
216
- { status: 500 }
217
- );
218
- }
219
- }
220
-
221
- /**
222
- * Express-compatible handler
223
- * For use with Express.js or other Node.js frameworks
224
- */
225
- export function telemetryHandler(req: any, res: any): void {
226
- // This is a simplified version for Express
227
- // In a real Express setup, you'd need to adapt the NextRequest/NextResponse types
228
- const handler = async () => {
229
- try {
230
- await ensureLogDir();
231
- await rotateLogIfNeeded();
232
-
233
- const body = req.body;
234
- const entries = Array.isArray(body) ? body : [body];
235
-
236
- if (entries.length > MAX_BATCH_SIZE) {
237
- return res.status(400).json({
238
- success: false,
239
- error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}`
240
- });
241
- }
242
-
243
- const validEntries: TelemetryEntry[] = [];
244
- for (const entry of entries) {
245
- const validated = validateEntry(entry);
246
- if (validated) {
247
- // Extract from Express request
248
- validated.userId = validated.userId || req.headers['x-user-id'] || req.user?.id;
249
- validated.sessionId = validated.sessionId || req.headers['x-session-id'] || req.session?.id;
250
- validEntries.push(validated);
251
- }
252
- }
253
-
254
- if (validEntries.length === 0) {
255
- return res.status(400).json({
256
- success: false,
257
- error: 'No valid entries to log'
258
- });
259
- }
260
-
261
- const logLines = validEntries.map(formatLogEntry).join('');
262
- await fs.appendFile(LOG_FILE, logLines, 'utf8');
263
-
264
- res.json({
265
- success: true,
266
- logged: validEntries.length,
267
- rejected: entries.length - validEntries.length
268
- });
269
- } catch (error) {
270
- console.error('[Telemetry Handler] Error writing log:', error);
271
- res.status(500).json({ success: false, error: 'Failed to write log' });
272
- }
273
- };
274
-
275
- handler();
276
- }
277
-
package/src/api/route.ts DELETED
@@ -1,11 +0,0 @@
1
- import 'server-only';
2
-
3
- /**
4
- * Telemetry API Route Handler
5
- * Server-only wrapper for the telemetry handler
6
- * This file should ONLY be used in Next.js API routes
7
- */
8
- import { POST as handlerPOST } from './handler';
9
-
10
- export { handlerPOST as POST };
11
-
package/src/server.ts DELETED
@@ -1,380 +0,0 @@
1
- import 'server-only';
2
-
3
- /**
4
- * Server-only exports for @jhits/plugin-telemetry
5
- * These exports should only be used in server-side code (API routes, server components)
6
- */
7
-
8
- // Type definition to avoid circular dependency
9
- interface PluginRegistration {
10
- name: string;
11
- version: string;
12
- description?: string;
13
- routes: Array<{ path: string; component: string }>;
14
- apiHandlers: Array<{ method: string; path: string; handler: string }>;
15
- cssFiles: string[];
16
- setup?: () => Promise<void>;
17
- dependencies?: string[];
18
- config?: Record<string, any>;
19
- }
20
-
21
- import { NextRequest, NextResponse } from 'next/server';
22
- import fs from 'fs/promises';
23
- import path from 'path';
24
-
25
- const LOG_DIR = path.join(process.cwd(), 'logs');
26
- const LOG_FILE = path.join(LOG_DIR, 'jhits-system.log');
27
- const OLD_LOG_FILE = path.join(LOG_DIR, 'jhits-system.old.log');
28
- const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
29
- const MAX_BATCH_SIZE = 100; // Maximum entries per request
30
- const MAX_ENTRY_SIZE = 100 * 1024; // 100KB per entry
31
-
32
- interface TelemetryEntry {
33
- id: string;
34
- timestamp: number;
35
- category: string;
36
- message: string;
37
- data: any;
38
- stack?: string;
39
- context?: string;
40
- userId?: string;
41
- sessionId?: string;
42
- }
43
-
44
- /**
45
- * Ensure logs directory exists
46
- */
47
- async function ensureLogDir(): Promise<void> {
48
- try {
49
- await fs.access(LOG_DIR);
50
- } catch {
51
- await fs.mkdir(LOG_DIR, { recursive: true });
52
- }
53
- }
54
-
55
- /**
56
- * Check log file size and rotate if necessary
57
- */
58
- async function rotateLogIfNeeded(): Promise<void> {
59
- try {
60
- const stats = await fs.stat(LOG_FILE);
61
- if (stats.size > MAX_LOG_SIZE) {
62
- // Rename current log to old log
63
- try {
64
- await fs.rename(LOG_FILE, OLD_LOG_FILE);
65
- } catch (error: any) {
66
- // If old log exists, remove it first
67
- if (error.code === 'EEXIST' || error.code === 'ENOENT') {
68
- try {
69
- await fs.unlink(OLD_LOG_FILE);
70
- await fs.rename(LOG_FILE, OLD_LOG_FILE);
71
- } catch {
72
- // If still fails, just overwrite
73
- await fs.copyFile(LOG_FILE, OLD_LOG_FILE);
74
- await fs.unlink(LOG_FILE);
75
- }
76
- } else {
77
- throw error;
78
- }
79
- }
80
- }
81
- } catch (error: any) {
82
- // If file doesn't exist, that's fine - we'll create it
83
- if (error.code !== 'ENOENT') {
84
- console.error('[Telemetry Handler] Error checking log size:', error);
85
- }
86
- }
87
- }
88
-
89
- /**
90
- * Main API handler for telemetry plugin
91
- */
92
- export async function handleApi(
93
- req: NextRequest,
94
- path: string[],
95
- config: any
96
- ): Promise<NextResponse> {
97
- try {
98
- const { searchParams } = new URL(req.url);
99
- const action = path[0] || 'log';
100
-
101
- switch (action) {
102
- case 'log':
103
- const data = await req.json();
104
- const result = await telemetryHandler(data, {
105
- userId: req.headers.get('x-user-id') || undefined,
106
- sessionId: req.headers.get('x-session-id') || undefined
107
- });
108
- return NextResponse.json(result);
109
-
110
- case 'logs':
111
- return await getLogsHandler(req, path, config);
112
-
113
- default:
114
- return NextResponse.json({
115
- success: false,
116
- error: 'Unknown telemetry action'
117
- }, { status: 404 });
118
- }
119
- } catch (error) {
120
- console.error('[Telemetry API] Error:', error);
121
- return NextResponse.json({
122
- success: false,
123
- error: 'Internal server error'
124
- }, { status: 500 });
125
- }
126
- }
127
-
128
- /**
129
- * Plugin registration for auto-configuration
130
- */
131
- export async function registerPlugin(): Promise<PluginRegistration> {
132
- return {
133
- name: 'telemetry',
134
- version: '0.0.1',
135
- description: 'System telemetry and logging plugin',
136
- routes: [
137
- {
138
- path: '/dashboard/telemetry',
139
- component: 'TelemetryDashboard'
140
- }
141
- ],
142
- apiHandlers: [
143
- {
144
- method: 'POST',
145
- path: '/log',
146
- handler: 'telemetryHandler'
147
- },
148
- {
149
- method: 'GET',
150
- path: '/logs',
151
- handler: 'getLogsHandler'
152
- }
153
- ],
154
- cssFiles: ['telemetry-styles.css'],
155
- setup: async () => {
156
- // Create logs directory
157
- const { mkdir } = await import('fs/promises');
158
- const { join } = await import('path');
159
- try {
160
- await mkdir(join(process.cwd(), 'logs'), { recursive: true });
161
- } catch (error) {
162
- // Directory might already exist
163
- }
164
- },
165
- dependencies: [],
166
- config: {
167
- maxLogSize: 10 * 1024 * 1024, // 10MB
168
- maxBatchSize: 100,
169
- maxEntrySize: 100 * 1024 // 100KB
170
- }
171
- };
172
- }
173
-
174
- /**
175
- * API handler for retrieving logs
176
- */
177
- export async function getLogsHandler(
178
- req: NextRequest,
179
- path: string[],
180
- config: any
181
- ): Promise<NextResponse> {
182
- try {
183
- const fs = await import('fs/promises');
184
- const path = await import('path');
185
- const LOG_FILE = path.join(process.cwd(), 'logs', 'jhits-system.log');
186
-
187
- const { searchParams } = new URL(req.url);
188
- const lines = parseInt(searchParams.get('lines') || '100');
189
- const category = searchParams.get('category');
190
-
191
- // Read log file
192
- const logContent = await fs.readFile(LOG_FILE, 'utf8');
193
- const logLines = logContent.split('\n').filter(line => line.trim());
194
-
195
- // Filter by category if specified
196
- const filteredLines = category
197
- ? logLines.filter(line => line.includes(`[${category}]`))
198
- : logLines;
199
-
200
- // Get last N lines
201
- const recentLines = filteredLines.slice(-lines);
202
-
203
- return NextResponse.json({
204
- success: true,
205
- logs: recentLines,
206
- total: filteredLines.length
207
- });
208
- } catch (error) {
209
- return NextResponse.json({
210
- success: false,
211
- error: 'Failed to read logs',
212
- logs: []
213
- }, { status: 500 });
214
- }
215
- }
216
-
217
- /**
218
- * Format log entry as single-line JSON (or pretty-printed in debug mode)
219
- * CRITICAL: Single-line format for easier grep/tailing, but pretty-printed if debug flag is on
220
- */
221
- function formatLogEntry(entry: TelemetryEntry, debug: boolean = false): string {
222
- const timestamp = new Date(entry.timestamp).toISOString();
223
- const context = entry.context || 'unknown';
224
- const userId = entry.userId || 'anonymous';
225
- const sessionId = entry.sessionId || 'unknown';
226
-
227
- // Check for debug mode via environment variable
228
- const isDebugMode = debug || process.env.TELEMETRY_DEBUG === 'true';
229
-
230
- if (isDebugMode) {
231
- // Pretty-printed format for debugging
232
- const logObject = {
233
- timestamp,
234
- context,
235
- userId,
236
- sessionId,
237
- category: entry.category,
238
- message: entry.message,
239
- data: entry.data,
240
- ...(entry.stack && { stack: entry.stack })
241
- };
242
- return JSON.stringify(logObject, null, 2) + '\n';
243
- } else {
244
- // Single-line JSON format for production (easier grep/tailing)
245
- const sanitizedData = typeof entry.data === 'object'
246
- ? JSON.stringify(entry.data).replace(/\n/g, '\\n').replace(/\r/g, '\\r')
247
- : String(entry.data);
248
-
249
- const stackStr = entry.stack ? ` "stack":"${entry.stack.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"` : '';
250
-
251
- return `[${timestamp}] [${context}] [${userId}] [${sessionId}] [${entry.category}] ${entry.message} ${sanitizedData}${stackStr}\n`;
252
- }
253
- }
254
-
255
- /**
256
- * Validate and sanitize telemetry entry
257
- */
258
- function validateEntry(entry: any): TelemetryEntry | null {
259
- // Basic validation
260
- if (!entry || typeof entry !== 'object') {
261
- return null;
262
- }
263
-
264
- // Check required fields
265
- if (!entry.id || !entry.timestamp || !entry.category || !entry.message) {
266
- return null;
267
- }
268
-
269
- // Size check to prevent log injection attacks
270
- const entrySize = JSON.stringify(entry).length;
271
- if (entrySize > MAX_ENTRY_SIZE) {
272
- console.warn('[Telemetry Handler] Entry too large, skipping:', entry.id);
273
- return null;
274
- }
275
-
276
- // Sanitize string fields
277
- return {
278
- id: String(entry.id).substring(0, 100),
279
- timestamp: Number(entry.timestamp),
280
- category: String(entry.category).substring(0, 50),
281
- message: String(entry.message).substring(0, 500),
282
- data: entry.data || {},
283
- stack: entry.stack ? String(entry.stack).substring(0, 5000) : undefined,
284
- context: entry.context ? String(entry.context).substring(0, 100) : undefined,
285
- userId: entry.userId ? String(entry.userId).substring(0, 100) : undefined,
286
- sessionId: entry.sessionId ? String(entry.sessionId).substring(0, 100) : undefined,
287
- };
288
- }
289
-
290
- /**
291
- * Extract user/session info from request headers or cookies
292
- */
293
- function extractContext(request: NextRequest): { userId?: string; sessionId?: string } {
294
- // Try to get user ID from headers (custom header)
295
- const userId = request.headers.get('x-user-id') ||
296
- request.headers.get('x-userid') ||
297
- undefined;
298
-
299
- // Try to get session ID from headers or cookies
300
- const sessionId = request.headers.get('x-session-id') ||
301
- request.headers.get('x-sessionid') ||
302
- request.cookies.get('sessionId')?.value ||
303
- request.cookies.get('session')?.value ||
304
- undefined;
305
-
306
- return { userId, sessionId };
307
- }
308
-
309
- /**
310
- * Telemetry Handler
311
- * Server-only function for processing telemetry data
312
- *
313
- * @param data - Telemetry entry or array of entries
314
- * @param context - Optional context with userId and sessionId
315
- * @returns Result object with success status and logging information
316
- */
317
- export async function telemetryHandler(
318
- data: any,
319
- context?: { userId?: string; sessionId?: string }
320
- ): Promise<{ success: boolean; logged?: number; rejected?: number; error?: string }> {
321
- try {
322
- // Ensure log directory exists
323
- await ensureLogDir();
324
-
325
- // Rotate log if needed
326
- await rotateLogIfNeeded();
327
-
328
- // Handle single entry or batch of entries
329
- const entries = Array.isArray(data) ? data : [data];
330
-
331
- // Rate limiting: Check batch size
332
- if (entries.length > MAX_BATCH_SIZE) {
333
- return {
334
- success: false,
335
- error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}`
336
- };
337
- }
338
-
339
- // Validate and sanitize entries
340
- const validEntries: TelemetryEntry[] = [];
341
- for (const entry of entries) {
342
- const validated = validateEntry(entry);
343
- if (validated) {
344
- // Add context if provided
345
- if (context?.userId && !validated.userId) {
346
- validated.userId = context.userId;
347
- }
348
- if (context?.sessionId && !validated.sessionId) {
349
- validated.sessionId = context.sessionId;
350
- }
351
- validEntries.push(validated);
352
- }
353
- }
354
-
355
- if (validEntries.length === 0) {
356
- return {
357
- success: false,
358
- error: 'No valid entries to log'
359
- };
360
- }
361
-
362
- // Format and append each entry (check for debug mode)
363
- const debugMode = process.env.TELEMETRY_DEBUG === 'true';
364
- const logLines = validEntries.map(entry => formatLogEntry(entry, debugMode)).join('');
365
- await fs.appendFile(LOG_FILE, logLines, 'utf8');
366
-
367
- return {
368
- success: true,
369
- logged: validEntries.length,
370
- rejected: entries.length - validEntries.length
371
- };
372
- } catch (error) {
373
- console.error('[Telemetry Handler] Error writing log:', error);
374
- return {
375
- success: false,
376
- error: 'Failed to write log',
377
- };
378
- }
379
- }
380
-
@@ -1,18 +0,0 @@
1
- /**
2
- * Type declarations for Next.js server types
3
- * This package is designed to be used in Next.js applications where these types are available
4
- */
5
-
6
- declare module 'next/server' {
7
- export interface NextRequest extends Request {
8
- cookies: {
9
- get(name: string): { value: string } | undefined;
10
- };
11
- headers: Headers;
12
- }
13
-
14
- export class NextResponse extends Response {
15
- static json(body: any, init?: ResponseInit): NextResponse;
16
- }
17
- }
18
-
package/src/types.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * Telemetry Types
3
- * Type definitions for the telemetry system
4
- */
5
-
6
- export type TelemetryCategory = 'DRAG_DROP' | 'STATE' | 'ERROR' | 'UI';
7
-
8
- export interface TelemetryEntry {
9
- id: string;
10
- timestamp: number;
11
- category: TelemetryCategory;
12
- message: string;
13
- data: any;
14
- stack?: string;
15
- context?: string;
16
- userId?: string;
17
- sessionId?: string;
18
- }
19
-
20
- export interface TelemetryService {
21
- log: (category: TelemetryCategory, message: string, data?: any, context?: string) => void;
22
- error: (message: string, error: Error | unknown, data?: any, context?: string) => void;
23
- exportLogs: () => void;
24
- clearLogs: () => void;
25
- getLogs: () => TelemetryEntry[];
26
- flush: () => Promise<void>;
27
- setContext: (context: string) => void;
28
- }
29
-
@@ -1,111 +0,0 @@
1
- /**
2
- * Log Cleaner Utilities
3
- * Optimizes telemetry payloads by summarizing large data structures
4
- */
5
-
6
- // Use a generic interface to avoid circular dependencies
7
- interface BlockLike {
8
- id: string | number;
9
- type?: string;
10
- nestedBlocks?: BlockLike[];
11
- }
12
-
13
- /**
14
- * Summarized block representation for telemetry
15
- */
16
- export interface BlockSummary {
17
- id: string;
18
- type: string;
19
- childCount: number;
20
- }
21
-
22
- /**
23
- * Summarize a single block to reduce log payload size
24
- * Only includes essential information: id, type, and child count
25
- */
26
- export function summarizeBlock(block: BlockLike | { id: string; type: string; nestedBlocks?: any[] }): BlockSummary {
27
- return {
28
- id: String(block.id),
29
- type: String(block.type || 'unknown'),
30
- childCount: Array.isArray(block.nestedBlocks) ? block.nestedBlocks.length : 0
31
- };
32
- }
33
-
34
- /**
35
- * Summarize an array of blocks to reduce log payload size
36
- * Recursively summarizes nested blocks up to a maximum depth
37
- */
38
- export function summarizeBlocks(
39
- blocks: BlockLike[] | undefined,
40
- maxDepth: number = 2
41
- ): BlockSummary[] {
42
- if (!Array.isArray(blocks) || blocks.length === 0) {
43
- return [];
44
- }
45
-
46
- return blocks.map(block => {
47
- const summary: BlockSummary = {
48
- id: String(block.id),
49
- type: String(block.type || 'unknown'),
50
- childCount: Array.isArray(block.nestedBlocks) ? block.nestedBlocks.length : 0
51
- };
52
-
53
- // Optionally include nested summaries if depth allows (for debugging)
54
- if (maxDepth > 0 && block.nestedBlocks && block.nestedBlocks.length > 0) {
55
- // Only include nested summaries in debug mode
56
- // For production, just the childCount is sufficient
57
- }
58
-
59
- return summary;
60
- });
61
- }
62
-
63
- /**
64
- * Extract action-specific data for telemetry
65
- */
66
- export interface MoveBlockData {
67
- blockId: string;
68
- fromParent: string | null;
69
- toParent: string | null;
70
- fromIndex?: number;
71
- toIndex: number;
72
- }
73
-
74
- export interface DeleteBlockData {
75
- blockId: string;
76
- blockType: string;
77
- parentId: string | null;
78
- }
79
-
80
- export interface InsertBlockData {
81
- newBlockType: string;
82
- parentId: string | null;
83
- index: number;
84
- newBlockId?: string;
85
- }
86
-
87
- /**
88
- * Create optimized telemetry payload for block operations
89
- * Removes full block arrays and only includes essential action data
90
- */
91
- export function createBlockActionPayload(
92
- actionType: 'MOVE_BLOCK' | 'DELETE_BLOCK' | 'INSERT_BLOCK',
93
- data: MoveBlockData | DeleteBlockData | InsertBlockData,
94
- options?: {
95
- includeBlocks?: boolean; // Only for CRITICAL_ERROR
96
- affectedBlockId?: string;
97
- }
98
- ): Record<string, any> {
99
- const payload: Record<string, any> = {
100
- actionType,
101
- ...data
102
- };
103
-
104
- // Only include full blocks for critical errors
105
- if (options?.includeBlocks && options.affectedBlockId) {
106
- payload.affectedBlockId = options.affectedBlockId;
107
- }
108
-
109
- return payload;
110
- }
111
-