@jhits/plugin-telemetry 0.0.4 → 0.0.6

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.
@@ -0,0 +1,8 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { TelemetryService as ITelemetryService } from './types';
3
+ export interface TelemetryProviderProps {
4
+ children: ReactNode;
5
+ }
6
+ export declare const TelemetryProvider: React.FC<TelemetryProviderProps>;
7
+ export declare const useTelemetry: () => ITelemetryService;
8
+ //# sourceMappingURL=TelemetryProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TelemetryProvider.d.ts","sourceRoot":"","sources":["../src/TelemetryProvider.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,EAA6B,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,EAAE,gBAAgB,IAAI,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAKhE,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,SAAS,CAAC;CACvB;AAED,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAM9D,CAAC;AAEF,eAAO,MAAM,YAAY,QAAO,iBAO/B,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Telemetry Provider
3
+ * React context provider for telemetry functionality
4
+ */
5
+ "use client";
6
+ import { jsx as _jsx } from "react/jsx-runtime";
7
+ import { createContext, useContext } from 'react';
8
+ import { telemetryService } from './TelemetryService';
9
+ const TelemetryContext = createContext(null);
10
+ export const TelemetryProvider = ({ children }) => {
11
+ return (_jsx(TelemetryContext.Provider, { value: telemetryService, children: children }));
12
+ };
13
+ export const useTelemetry = () => {
14
+ const context = useContext(TelemetryContext);
15
+ if (!context) {
16
+ // Fallback to service directly if context is not available
17
+ return telemetryService;
18
+ }
19
+ return context;
20
+ };
@@ -0,0 +1,42 @@
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
+ declare class TelemetryServiceClass implements ITelemetryService {
8
+ private buffer;
9
+ private flushTimer;
10
+ private defaultContext;
11
+ private generateId;
12
+ /**
13
+ * Safe JSON stringify that handles circular references and DOM/React elements
14
+ * Uses WeakSet to track seen objects and returns "[Circular]" for repeated references
15
+ */
16
+ private safeStringify;
17
+ /**
18
+ * Get safe JSON serializer that handles circular references and React Fiber nodes
19
+ */
20
+ /**
21
+ * Send entries to backend API
22
+ * CRITICAL: Uses safe serializer to prevent circular JSON errors
23
+ */
24
+ private sendToBackend;
25
+ /**
26
+ * Schedule a flush of the buffer
27
+ */
28
+ private scheduleFlush;
29
+ /**
30
+ * Flush buffer to backend immediately
31
+ */
32
+ flush(useBeacon?: boolean): Promise<void>;
33
+ log(category: TelemetryCategory, message: string, data?: any, context?: string): void;
34
+ error(message: string, error: Error | unknown, data?: any, context?: string): void;
35
+ setContext(context: string): void;
36
+ exportLogs(): void;
37
+ clearLogs(): void;
38
+ getLogs(): TelemetryEntry[];
39
+ }
40
+ export declare const telemetryService: TelemetryServiceClass;
41
+ export {};
42
+ //# sourceMappingURL=TelemetryService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TelemetryService.d.ts","sourceRoot":"","sources":["../src/TelemetryService.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,gBAAgB,IAAI,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAMnG,cAAM,qBAAsB,YAAW,iBAAiB;IACpD,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,cAAc,CAAoB;IAE1C,OAAO,CAAC,UAAU;IAIlB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAkFrB;;OAEG;IACH;;;OAGG;YACW,aAAa;IAsC3B;;OAEG;IACH,OAAO,CAAC,aAAa;IAYrB;;OAEG;IACG,KAAK,CAAC,SAAS,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IActD,GAAG,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAkCrF,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IA4BlF,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIjC,UAAU,IAAI,IAAI;IAUlB,SAAS,IAAI,IAAI;IAajB,OAAO,IAAI,cAAc,EAAE;CAI9B;AAoDD,eAAO,MAAM,gBAAgB,uBAA8B,CAAC"}
@@ -0,0 +1,285 @@
1
+ const API_ENDPOINT = '/api/dashboard/telemetry';
2
+ const BUFFER_INTERVAL_MS = 5000; // Send buffered logs every 5 seconds
3
+ const MAX_BUFFER_SIZE = 50; // Max entries to buffer before forcing send
4
+ class TelemetryServiceClass {
5
+ constructor() {
6
+ this.buffer = [];
7
+ this.flushTimer = null;
8
+ this.defaultContext = 'system';
9
+ }
10
+ generateId() {
11
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
12
+ }
13
+ /**
14
+ * Safe JSON stringify that handles circular references and DOM/React elements
15
+ * Uses WeakSet to track seen objects and returns "[Circular]" for repeated references
16
+ */
17
+ safeStringify(value, space) {
18
+ const seen = new WeakSet();
19
+ const isDOMNode = (val) => {
20
+ return (typeof Node !== 'undefined' && val instanceof Node ||
21
+ typeof Element !== 'undefined' && val instanceof Element ||
22
+ (val && typeof val === 'object' && 'nodeType' in val && 'nodeName' in val));
23
+ };
24
+ const isReactElement = (val) => {
25
+ return (val &&
26
+ typeof val === 'object' &&
27
+ (val.$$typeof === Symbol.for('react.element') ||
28
+ val.$$typeof === Symbol.for('react.portal') ||
29
+ Object.keys(val).some(key => key.startsWith('__react') || key.startsWith('_react'))));
30
+ };
31
+ const isEvent = (val) => {
32
+ return (val &&
33
+ typeof val === 'object' &&
34
+ (val instanceof Event ||
35
+ (typeof Event !== 'undefined' && val.constructor?.name === 'Event') ||
36
+ (val.target && val.currentTarget && val.type)));
37
+ };
38
+ const replacer = (key, val) => {
39
+ // Skip React internal keys
40
+ if (key.startsWith('__react') || key.startsWith('_react')) {
41
+ return '[React Internal]';
42
+ }
43
+ // Handle null/undefined
44
+ if (val === null || val === undefined) {
45
+ return val;
46
+ }
47
+ // Handle DOM nodes
48
+ if (isDOMNode(val)) {
49
+ return '[DOM Node]';
50
+ }
51
+ // Handle React elements
52
+ if (isReactElement(val)) {
53
+ return '[React Element]';
54
+ }
55
+ // Handle Events
56
+ if (isEvent(val)) {
57
+ return `[Event: ${val.type || 'unknown'}]`;
58
+ }
59
+ // Handle functions
60
+ if (typeof val === 'function') {
61
+ return `[Function: ${val.name || 'anonymous'}]`;
62
+ }
63
+ // Handle circular references
64
+ if (typeof val === 'object') {
65
+ if (seen.has(val)) {
66
+ return '[Circular]';
67
+ }
68
+ seen.add(val);
69
+ }
70
+ return val;
71
+ };
72
+ try {
73
+ return JSON.stringify(value, replacer, space);
74
+ }
75
+ catch (error) {
76
+ // Fallback if stringify still fails
77
+ return JSON.stringify({
78
+ error: 'Failed to stringify',
79
+ message: error instanceof Error ? error.message : String(error),
80
+ type: typeof value,
81
+ }, null, space);
82
+ }
83
+ }
84
+ /**
85
+ * Get safe JSON serializer that handles circular references and React Fiber nodes
86
+ */
87
+ /**
88
+ * Send entries to backend API
89
+ * CRITICAL: Uses safe serializer to prevent circular JSON errors
90
+ */
91
+ async sendToBackend(entries, useBeacon = false) {
92
+ if (entries.length === 0)
93
+ return;
94
+ try {
95
+ // SAFE SERIALIZER to prevent Circular structure errors
96
+ const getCircularReplacer = () => {
97
+ const seen = new WeakSet();
98
+ return (key, value) => {
99
+ if (typeof value === "object" && value !== null) {
100
+ if (seen.has(value))
101
+ return "[Circular]";
102
+ seen.add(value);
103
+ // Strip out DOM elements and React internals
104
+ if (value instanceof Node)
105
+ return `[DOM: ${value.nodeName}]`;
106
+ if (key.startsWith('__react'))
107
+ return "[ReactInternal]";
108
+ }
109
+ return value;
110
+ };
111
+ };
112
+ const data = JSON.stringify(entries, getCircularReplacer());
113
+ if (navigator.sendBeacon && useBeacon) {
114
+ const blob = new Blob([data], { type: 'application/json' });
115
+ if (navigator.sendBeacon(API_ENDPOINT, blob))
116
+ return;
117
+ }
118
+ const response = await fetch(API_ENDPOINT, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: data,
122
+ });
123
+ if (!response.ok)
124
+ throw new Error(`Status ${response.status}`);
125
+ }
126
+ catch (error) {
127
+ console.error('[TelemetryService] Serialization or Network failure:', error);
128
+ }
129
+ }
130
+ /**
131
+ * Schedule a flush of the buffer
132
+ */
133
+ scheduleFlush() {
134
+ // Clear existing timer
135
+ if (this.flushTimer) {
136
+ clearTimeout(this.flushTimer);
137
+ }
138
+ // Schedule new flush
139
+ this.flushTimer = setTimeout(() => {
140
+ this.flush();
141
+ }, BUFFER_INTERVAL_MS);
142
+ }
143
+ /**
144
+ * Flush buffer to backend immediately
145
+ */
146
+ async flush(useBeacon = false) {
147
+ if (this.buffer.length === 0)
148
+ return;
149
+ const entriesToSend = [...this.buffer];
150
+ this.buffer = [];
151
+ if (this.flushTimer) {
152
+ clearTimeout(this.flushTimer);
153
+ this.flushTimer = null;
154
+ }
155
+ await this.sendToBackend(entriesToSend, useBeacon);
156
+ }
157
+ log(category, message, data, context) {
158
+ const entry = {
159
+ id: this.generateId(),
160
+ timestamp: Date.now(),
161
+ category,
162
+ message,
163
+ data: data || {},
164
+ context: context || this.defaultContext,
165
+ };
166
+ // Add to buffer
167
+ this.buffer.push(entry);
168
+ // Also log to console for immediate visibility (less intrusive, emoji-coded breadcrumbs)
169
+ // Only log in development or if explicitly enabled
170
+ if (process.env.NODE_ENV === 'development' || process.env.TELEMETRY_VERBOSE === 'true') {
171
+ const emoji = category === 'DRAG_DROP' ? '📦' : category === 'STATE' ? '📊' : category === 'ERROR' ? '❌' : '🎨';
172
+ // Use safeStringify for console logs too to prevent circular reference errors
173
+ const safeData = data ? this.safeStringify(data) : '';
174
+ console.log(`${emoji} [${entry.context}] ${message}`, safeData || '');
175
+ }
176
+ // Send immediately if ERROR, otherwise schedule flush
177
+ if (category === 'ERROR') {
178
+ this.flush();
179
+ }
180
+ else if (this.buffer.length >= MAX_BUFFER_SIZE) {
181
+ // Force flush if buffer is getting large
182
+ this.flush();
183
+ }
184
+ else {
185
+ // Schedule regular flush
186
+ this.scheduleFlush();
187
+ }
188
+ }
189
+ error(message, error, data, context) {
190
+ const errorObj = error instanceof Error ? error : new Error(String(error));
191
+ const entry = {
192
+ id: this.generateId(),
193
+ timestamp: Date.now(),
194
+ category: 'ERROR',
195
+ message,
196
+ data: {
197
+ ...data,
198
+ errorMessage: errorObj.message,
199
+ errorName: errorObj.name,
200
+ },
201
+ stack: errorObj.stack,
202
+ context: context || this.defaultContext,
203
+ };
204
+ // Add to buffer
205
+ this.buffer.push(entry);
206
+ // Also log to console for immediate visibility (errors always shown, but with safe stringify)
207
+ // Use safeStringify to prevent circular reference errors in console
208
+ const safeData = data ? this.safeStringify(data) : '';
209
+ console.error(`❌ [${entry.context}] ${message}`, error instanceof Error ? error.message : String(error), safeData || '');
210
+ // Errors are sent immediately
211
+ this.flush();
212
+ }
213
+ setContext(context) {
214
+ this.defaultContext = context;
215
+ }
216
+ exportLogs() {
217
+ // Flush any pending logs first
218
+ this.flush().then(() => {
219
+ // Inform user that logs are on the server
220
+ alert('Telemetry logs are stored on the server at: logs/editor-debug.log\n\nCheck the server logs directory for the file.');
221
+ }).catch(() => {
222
+ alert('Telemetry logs are stored on the server. Check logs/editor-debug.log on the server.');
223
+ });
224
+ }
225
+ clearLogs() {
226
+ // Clear the buffer (logs are already on server, can't clear server logs from client)
227
+ this.buffer = [];
228
+ if (this.flushTimer) {
229
+ clearTimeout(this.flushTimer);
230
+ this.flushTimer = null;
231
+ }
232
+ // Less intrusive console log
233
+ if (process.env.NODE_ENV === 'development' || process.env.TELEMETRY_VERBOSE === 'true') {
234
+ console.log('🧹 Telemetry buffer cleared');
235
+ }
236
+ }
237
+ getLogs() {
238
+ // Return current buffer (server logs are not accessible from client)
239
+ return [...this.buffer];
240
+ }
241
+ }
242
+ // Initialize global error handlers
243
+ const initializeGlobalErrorHandlers = (service) => {
244
+ // Capture window.onerror
245
+ const originalOnError = window.onerror;
246
+ window.onerror = (message, source, lineno, colno, error) => {
247
+ service.error('Global Error Handler', error || new Error(String(message)), {
248
+ message: String(message),
249
+ source: String(source),
250
+ lineno,
251
+ colno,
252
+ });
253
+ if (originalOnError) {
254
+ return originalOnError(message, source, lineno, colno, error);
255
+ }
256
+ return false;
257
+ };
258
+ // Capture unhandled promise rejections
259
+ window.addEventListener('unhandledrejection', (event) => {
260
+ service.error('Unhandled Promise Rejection', event.reason, {
261
+ // Don't include the promise object itself (can cause circular references)
262
+ reason: event.reason instanceof Error ? event.reason.message : String(event.reason),
263
+ });
264
+ });
265
+ };
266
+ // Initialize unload handler for flushing logs
267
+ const initializeUnloadHandler = (service) => {
268
+ // Flush logs on page unload using sendBeacon
269
+ window.addEventListener('beforeunload', () => {
270
+ service.flush(true); // Use sendBeacon for unload
271
+ });
272
+ // Also handle visibility change (tab switch, minimize, etc.)
273
+ document.addEventListener('visibilitychange', () => {
274
+ if (document.visibilityState === 'hidden') {
275
+ service.flush(true); // Use sendBeacon when tab becomes hidden
276
+ }
277
+ });
278
+ };
279
+ // Create singleton instance
280
+ export const telemetryService = new TelemetryServiceClass();
281
+ // Initialize global error handlers and unload handler
282
+ if (typeof window !== 'undefined') {
283
+ initializeGlobalErrorHandlers(telemetryService);
284
+ initializeUnloadHandler(telemetryService);
285
+ }
@@ -0,0 +1,20 @@
1
+ import 'server-only';
2
+ /**
3
+ * Telemetry API Handler
4
+ * Plugin-mounted API handler for telemetry logging
5
+ * Compatible with Next.js API routes and Express
6
+ *
7
+ * IMPORTANT: This file should ONLY be imported in server-side API routes.
8
+ * Do NOT import this in client-side code.
9
+ */
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ /**
12
+ * Next.js API Route Handler
13
+ */
14
+ export declare function POST(request: NextRequest): Promise<NextResponse>;
15
+ /**
16
+ * Express-compatible handler
17
+ * For use with Express.js or other Node.js frameworks
18
+ */
19
+ export declare function telemetryHandler(req: any, res: any): void;
20
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;;;;GAOG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6IxD;;GAEG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAgEtE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAmDzD"}
@@ -0,0 +1,229 @@
1
+ import 'server-only';
2
+ /**
3
+ * Telemetry API Handler
4
+ * Plugin-mounted API handler for telemetry logging
5
+ * Compatible with Next.js API routes and Express
6
+ *
7
+ * IMPORTANT: This file should ONLY be imported in server-side API routes.
8
+ * Do NOT import this in client-side code.
9
+ */
10
+ import { NextResponse } from 'next/server';
11
+ import fs from 'fs/promises';
12
+ import path from 'path';
13
+ const LOG_DIR = path.join(process.cwd(), 'logs');
14
+ const LOG_FILE = path.join(LOG_DIR, 'jhits-system.log');
15
+ const OLD_LOG_FILE = path.join(LOG_DIR, 'jhits-system.old.log');
16
+ const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
17
+ const MAX_BATCH_SIZE = 100; // Maximum entries per request
18
+ const MAX_ENTRY_SIZE = 100 * 1024; // 100KB per entry
19
+ /**
20
+ * Ensure logs directory exists
21
+ */
22
+ async function ensureLogDir() {
23
+ try {
24
+ await fs.access(LOG_DIR);
25
+ }
26
+ catch {
27
+ await fs.mkdir(LOG_DIR, { recursive: true });
28
+ }
29
+ }
30
+ /**
31
+ * Check log file size and rotate if necessary
32
+ */
33
+ async function rotateLogIfNeeded() {
34
+ try {
35
+ const stats = await fs.stat(LOG_FILE);
36
+ if (stats.size > MAX_LOG_SIZE) {
37
+ // Rename current log to old log
38
+ try {
39
+ await fs.rename(LOG_FILE, OLD_LOG_FILE);
40
+ }
41
+ catch (error) {
42
+ // If old log exists, remove it first
43
+ if (error.code === 'EEXIST' || error.code === 'ENOENT') {
44
+ try {
45
+ await fs.unlink(OLD_LOG_FILE);
46
+ await fs.rename(LOG_FILE, OLD_LOG_FILE);
47
+ }
48
+ catch {
49
+ // If still fails, just overwrite
50
+ await fs.copyFile(LOG_FILE, OLD_LOG_FILE);
51
+ await fs.unlink(LOG_FILE);
52
+ }
53
+ }
54
+ else {
55
+ throw error;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ catch (error) {
61
+ // If file doesn't exist, that's fine - we'll create it
62
+ if (error.code !== 'ENOENT') {
63
+ console.error('[Telemetry Handler] Error checking log size:', error);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Format log entry as single-line JSON
69
+ */
70
+ function formatLogEntry(entry) {
71
+ const timestamp = new Date(entry.timestamp).toISOString();
72
+ const context = entry.context || 'unknown';
73
+ const userId = entry.userId || 'anonymous';
74
+ const sessionId = entry.sessionId || 'unknown';
75
+ // Sanitize data to prevent log injection
76
+ const sanitizedData = typeof entry.data === 'object'
77
+ ? JSON.stringify(entry.data).replace(/\n/g, '\\n').replace(/\r/g, '\\r')
78
+ : String(entry.data);
79
+ const stackStr = entry.stack ? ` "stack":"${entry.stack.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"` : '';
80
+ return `[${timestamp}] [${context}] [${userId}] [${sessionId}] [${entry.category}] ${entry.message} ${sanitizedData}${stackStr}\n`;
81
+ }
82
+ /**
83
+ * Validate and sanitize telemetry entry
84
+ */
85
+ function validateEntry(entry) {
86
+ // Basic validation
87
+ if (!entry || typeof entry !== 'object') {
88
+ return null;
89
+ }
90
+ // Check required fields
91
+ if (!entry.id || !entry.timestamp || !entry.category || !entry.message) {
92
+ return null;
93
+ }
94
+ // Size check to prevent log injection attacks
95
+ const entrySize = JSON.stringify(entry).length;
96
+ if (entrySize > MAX_ENTRY_SIZE) {
97
+ console.warn('[Telemetry Handler] Entry too large, skipping:', entry.id);
98
+ return null;
99
+ }
100
+ // Sanitize string fields
101
+ return {
102
+ id: String(entry.id).substring(0, 100),
103
+ timestamp: Number(entry.timestamp),
104
+ category: String(entry.category).substring(0, 50),
105
+ message: String(entry.message).substring(0, 500),
106
+ data: entry.data || {},
107
+ stack: entry.stack ? String(entry.stack).substring(0, 5000) : undefined,
108
+ context: entry.context ? String(entry.context).substring(0, 100) : undefined,
109
+ userId: entry.userId ? String(entry.userId).substring(0, 100) : undefined,
110
+ sessionId: entry.sessionId ? String(entry.sessionId).substring(0, 100) : undefined,
111
+ };
112
+ }
113
+ /**
114
+ * Extract user/session info from request headers or cookies
115
+ */
116
+ function extractContext(request) {
117
+ // Try to get user ID from headers (custom header)
118
+ const userId = request.headers.get('x-user-id') ||
119
+ request.headers.get('x-userid') ||
120
+ undefined;
121
+ // Try to get session ID from headers or cookies
122
+ const sessionId = request.headers.get('x-session-id') ||
123
+ request.headers.get('x-sessionid') ||
124
+ request.cookies.get('sessionId')?.value ||
125
+ request.cookies.get('session')?.value ||
126
+ undefined;
127
+ return { userId, sessionId };
128
+ }
129
+ /**
130
+ * Next.js API Route Handler
131
+ */
132
+ export async function POST(request) {
133
+ try {
134
+ // Ensure log directory exists
135
+ await ensureLogDir();
136
+ // Rotate log if needed
137
+ await rotateLogIfNeeded();
138
+ // Extract context from request
139
+ const { userId, sessionId } = extractContext(request);
140
+ // Parse request body
141
+ const body = await request.json();
142
+ // Handle single entry or batch of entries
143
+ const entries = Array.isArray(body) ? body : [body];
144
+ // Rate limiting: Check batch size
145
+ if (entries.length > MAX_BATCH_SIZE) {
146
+ return NextResponse.json({ success: false, error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}` }, { status: 400 });
147
+ }
148
+ // Validate and sanitize entries
149
+ const validEntries = [];
150
+ for (const entry of entries) {
151
+ const validated = validateEntry(entry);
152
+ if (validated) {
153
+ // Add context from request if not present in entry
154
+ if (!validated.userId && userId) {
155
+ validated.userId = userId;
156
+ }
157
+ if (!validated.sessionId && sessionId) {
158
+ validated.sessionId = sessionId;
159
+ }
160
+ validEntries.push(validated);
161
+ }
162
+ }
163
+ if (validEntries.length === 0) {
164
+ return NextResponse.json({ success: false, error: 'No valid entries to log' }, { status: 400 });
165
+ }
166
+ // Format and append each entry
167
+ const logLines = validEntries.map(formatLogEntry).join('');
168
+ await fs.appendFile(LOG_FILE, logLines, 'utf8');
169
+ return NextResponse.json({
170
+ success: true,
171
+ logged: validEntries.length,
172
+ rejected: entries.length - validEntries.length
173
+ });
174
+ }
175
+ catch (error) {
176
+ console.error('[Telemetry Handler] Error writing log:', error);
177
+ return NextResponse.json({ success: false, error: 'Failed to write log' }, { status: 500 });
178
+ }
179
+ }
180
+ /**
181
+ * Express-compatible handler
182
+ * For use with Express.js or other Node.js frameworks
183
+ */
184
+ export function telemetryHandler(req, res) {
185
+ // This is a simplified version for Express
186
+ // In a real Express setup, you'd need to adapt the NextRequest/NextResponse types
187
+ const handler = async () => {
188
+ try {
189
+ await ensureLogDir();
190
+ await rotateLogIfNeeded();
191
+ const body = req.body;
192
+ const entries = Array.isArray(body) ? body : [body];
193
+ if (entries.length > MAX_BATCH_SIZE) {
194
+ return res.status(400).json({
195
+ success: false,
196
+ error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}`
197
+ });
198
+ }
199
+ const validEntries = [];
200
+ for (const entry of entries) {
201
+ const validated = validateEntry(entry);
202
+ if (validated) {
203
+ // Extract from Express request
204
+ validated.userId = validated.userId || req.headers['x-user-id'] || req.user?.id;
205
+ validated.sessionId = validated.sessionId || req.headers['x-session-id'] || req.session?.id;
206
+ validEntries.push(validated);
207
+ }
208
+ }
209
+ if (validEntries.length === 0) {
210
+ return res.status(400).json({
211
+ success: false,
212
+ error: 'No valid entries to log'
213
+ });
214
+ }
215
+ const logLines = validEntries.map(formatLogEntry).join('');
216
+ await fs.appendFile(LOG_FILE, logLines, 'utf8');
217
+ res.json({
218
+ success: true,
219
+ logged: validEntries.length,
220
+ rejected: entries.length - validEntries.length
221
+ });
222
+ }
223
+ catch (error) {
224
+ console.error('[Telemetry Handler] Error writing log:', error);
225
+ res.status(500).json({ success: false, error: 'Failed to write log' });
226
+ }
227
+ };
228
+ handler();
229
+ }
@@ -0,0 +1,9 @@
1
+ import 'server-only';
2
+ /**
3
+ * Telemetry API Route Handler
4
+ * Server-only wrapper for the telemetry handler
5
+ * This file should ONLY be used in Next.js API routes
6
+ */
7
+ import { POST as handlerPOST } from './handler';
8
+ export { handlerPOST as POST };
9
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/api/route.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;GAIG;AACH,OAAO,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,EAAE,WAAW,IAAI,IAAI,EAAE,CAAC"}
@@ -0,0 +1,8 @@
1
+ import 'server-only';
2
+ /**
3
+ * Telemetry API Route Handler
4
+ * Server-only wrapper for the telemetry handler
5
+ * This file should ONLY be used in Next.js API routes
6
+ */
7
+ import { POST as handlerPOST } from './handler';
8
+ export { handlerPOST as POST };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Plugin Telemetry
3
+ * Standalone telemetry package for JHITS v2
4
+ *
5
+ * NOTE: API handler is NOT exported here to prevent client-side bundling.
6
+ * Import the handler directly from '@jhits/plugin-telemetry/api/handler' in server-side API routes only.
7
+ */
8
+ export { TelemetryProvider, useTelemetry } from './TelemetryProvider';
9
+ export { telemetryService } from './TelemetryService';
10
+ export type { TelemetryEntry, TelemetryCategory, TelemetryService } from './types';
11
+ export { summarizeBlock, summarizeBlocks, createBlockActionPayload } from './utils/logCleaner';
12
+ export type { BlockSummary, MoveBlockData, DeleteBlockData, InsertBlockData } from './utils/logCleaner';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC/F,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Plugin Telemetry
3
+ * Standalone telemetry package for JHITS v2
4
+ *
5
+ * NOTE: API handler is NOT exported here to prevent client-side bundling.
6
+ * Import the handler directly from '@jhits/plugin-telemetry/api/handler' in server-side API routes only.
7
+ */
8
+ export { TelemetryProvider, useTelemetry } from './TelemetryProvider';
9
+ export { telemetryService } from './TelemetryService';
10
+ export { summarizeBlock, summarizeBlocks, createBlockActionPayload } from './utils/logCleaner';