@jhits/plugin-telemetry 0.0.1 → 0.0.3
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 +36 -36
- package/src/api/handler.ts +40 -38
- package/src/api/route.ts +2 -0
- package/src/server.ts +22 -20
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhits/plugin-telemetry",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "System logging and telemetry utilities for the JHITS ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
|
-
|
|
6
|
+
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"types": "./src/index.ts",
|
|
10
10
|
"exports": {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.ts",
|
|
13
|
+
"default": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./src/server.ts",
|
|
17
|
+
"default": "./src/server.ts"
|
|
18
|
+
},
|
|
19
|
+
"./api/route": {
|
|
20
|
+
"types": "./src/api/route.ts",
|
|
21
|
+
"default": "./src/api/route.ts"
|
|
22
|
+
},
|
|
23
|
+
"./api/handler": {
|
|
24
|
+
"types": "./src/api/handler.ts",
|
|
25
|
+
"default": "./src/api/handler.ts"
|
|
26
|
+
},
|
|
27
|
+
"./utils/logCleaner": {
|
|
28
|
+
"types": "./src/utils/logCleaner.ts",
|
|
29
|
+
"default": "./src/utils/logCleaner.ts"
|
|
30
|
+
}
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
|
|
33
|
+
"@jhits/plugin-core": "^0.0.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
"next": ">=15.0.0",
|
|
37
|
+
"react": ">=18.0.0",
|
|
38
|
+
"react-dom": ">=18.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
"@types/node": "^20.19.27",
|
|
42
|
+
"@types/react": "^19",
|
|
43
|
+
"@types/react-dom": "^19",
|
|
44
|
+
"next": "16.1.1",
|
|
45
|
+
"react": "19.2.3",
|
|
46
|
+
"react-dom": "19.2.3",
|
|
47
|
+
"typescript": "^5"
|
|
48
48
|
},
|
|
49
49
|
"files": [
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
"src",
|
|
51
|
+
"package.json"
|
|
52
52
|
]
|
|
53
|
-
|
|
53
|
+
}
|
package/src/api/handler.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Telemetry API Handler
|
|
3
5
|
* Plugin-mounted API handler for telemetry logging
|
|
@@ -82,14 +84,14 @@ function formatLogEntry(entry: TelemetryEntry): string {
|
|
|
82
84
|
const context = entry.context || 'unknown';
|
|
83
85
|
const userId = entry.userId || 'anonymous';
|
|
84
86
|
const sessionId = entry.sessionId || 'unknown';
|
|
85
|
-
|
|
87
|
+
|
|
86
88
|
// Sanitize data to prevent log injection
|
|
87
|
-
const sanitizedData = typeof entry.data === 'object'
|
|
89
|
+
const sanitizedData = typeof entry.data === 'object'
|
|
88
90
|
? JSON.stringify(entry.data).replace(/\n/g, '\\n').replace(/\r/g, '\\r')
|
|
89
91
|
: String(entry.data);
|
|
90
|
-
|
|
92
|
+
|
|
91
93
|
const stackStr = entry.stack ? ` "stack":"${entry.stack.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"` : '';
|
|
92
|
-
|
|
94
|
+
|
|
93
95
|
return `[${timestamp}] [${context}] [${userId}] [${sessionId}] [${entry.category}] ${entry.message} ${sanitizedData}${stackStr}\n`;
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -133,16 +135,16 @@ function validateEntry(entry: any): TelemetryEntry | null {
|
|
|
133
135
|
*/
|
|
134
136
|
function extractContext(request: NextRequest): { userId?: string; sessionId?: string } {
|
|
135
137
|
// Try to get user ID from headers (custom header)
|
|
136
|
-
const userId = request.headers.get('x-user-id') ||
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const userId = request.headers.get('x-user-id') ||
|
|
139
|
+
request.headers.get('x-userid') ||
|
|
140
|
+
undefined;
|
|
141
|
+
|
|
140
142
|
// Try to get session ID from headers or cookies
|
|
141
143
|
const sessionId = request.headers.get('x-session-id') ||
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
request.headers.get('x-sessionid') ||
|
|
145
|
+
request.cookies.get('sessionId')?.value ||
|
|
146
|
+
request.cookies.get('session')?.value ||
|
|
147
|
+
undefined;
|
|
146
148
|
|
|
147
149
|
return { userId, sessionId };
|
|
148
150
|
}
|
|
@@ -154,19 +156,19 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
|
154
156
|
try {
|
|
155
157
|
// Ensure log directory exists
|
|
156
158
|
await ensureLogDir();
|
|
157
|
-
|
|
159
|
+
|
|
158
160
|
// Rotate log if needed
|
|
159
161
|
await rotateLogIfNeeded();
|
|
160
|
-
|
|
162
|
+
|
|
161
163
|
// Extract context from request
|
|
162
164
|
const { userId, sessionId } = extractContext(request);
|
|
163
|
-
|
|
165
|
+
|
|
164
166
|
// Parse request body
|
|
165
167
|
const body = await request.json();
|
|
166
|
-
|
|
168
|
+
|
|
167
169
|
// Handle single entry or batch of entries
|
|
168
170
|
const entries = Array.isArray(body) ? body : [body];
|
|
169
|
-
|
|
171
|
+
|
|
170
172
|
// Rate limiting: Check batch size
|
|
171
173
|
if (entries.length > MAX_BATCH_SIZE) {
|
|
172
174
|
return NextResponse.json(
|
|
@@ -174,7 +176,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
|
174
176
|
{ status: 400 }
|
|
175
177
|
);
|
|
176
178
|
}
|
|
177
|
-
|
|
179
|
+
|
|
178
180
|
// Validate and sanitize entries
|
|
179
181
|
const validEntries: TelemetryEntry[] = [];
|
|
180
182
|
for (const entry of entries) {
|
|
@@ -190,20 +192,20 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
|
190
192
|
validEntries.push(validated);
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
|
-
|
|
195
|
+
|
|
194
196
|
if (validEntries.length === 0) {
|
|
195
197
|
return NextResponse.json(
|
|
196
198
|
{ success: false, error: 'No valid entries to log' },
|
|
197
199
|
{ status: 400 }
|
|
198
200
|
);
|
|
199
201
|
}
|
|
200
|
-
|
|
202
|
+
|
|
201
203
|
// Format and append each entry
|
|
202
204
|
const logLines = validEntries.map(formatLogEntry).join('');
|
|
203
205
|
await fs.appendFile(LOG_FILE, logLines, 'utf8');
|
|
204
|
-
|
|
205
|
-
return NextResponse.json({
|
|
206
|
-
success: true,
|
|
206
|
+
|
|
207
|
+
return NextResponse.json({
|
|
208
|
+
success: true,
|
|
207
209
|
logged: validEntries.length,
|
|
208
210
|
rejected: entries.length - validEntries.length
|
|
209
211
|
});
|
|
@@ -227,17 +229,17 @@ export function telemetryHandler(req: any, res: any): void {
|
|
|
227
229
|
try {
|
|
228
230
|
await ensureLogDir();
|
|
229
231
|
await rotateLogIfNeeded();
|
|
230
|
-
|
|
232
|
+
|
|
231
233
|
const body = req.body;
|
|
232
234
|
const entries = Array.isArray(body) ? body : [body];
|
|
233
|
-
|
|
235
|
+
|
|
234
236
|
if (entries.length > MAX_BATCH_SIZE) {
|
|
235
|
-
return res.status(400).json({
|
|
236
|
-
success: false,
|
|
237
|
-
error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}`
|
|
237
|
+
return res.status(400).json({
|
|
238
|
+
success: false,
|
|
239
|
+
error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}`
|
|
238
240
|
});
|
|
239
241
|
}
|
|
240
|
-
|
|
242
|
+
|
|
241
243
|
const validEntries: TelemetryEntry[] = [];
|
|
242
244
|
for (const entry of entries) {
|
|
243
245
|
const validated = validateEntry(entry);
|
|
@@ -248,19 +250,19 @@ export function telemetryHandler(req: any, res: any): void {
|
|
|
248
250
|
validEntries.push(validated);
|
|
249
251
|
}
|
|
250
252
|
}
|
|
251
|
-
|
|
253
|
+
|
|
252
254
|
if (validEntries.length === 0) {
|
|
253
|
-
return res.status(400).json({
|
|
254
|
-
success: false,
|
|
255
|
-
error: 'No valid entries to log'
|
|
255
|
+
return res.status(400).json({
|
|
256
|
+
success: false,
|
|
257
|
+
error: 'No valid entries to log'
|
|
256
258
|
});
|
|
257
259
|
}
|
|
258
|
-
|
|
260
|
+
|
|
259
261
|
const logLines = validEntries.map(formatLogEntry).join('');
|
|
260
262
|
await fs.appendFile(LOG_FILE, logLines, 'utf8');
|
|
261
|
-
|
|
262
|
-
res.json({
|
|
263
|
-
success: true,
|
|
263
|
+
|
|
264
|
+
res.json({
|
|
265
|
+
success: true,
|
|
264
266
|
logged: validEntries.length,
|
|
265
267
|
rejected: entries.length - validEntries.length
|
|
266
268
|
});
|
|
@@ -269,7 +271,7 @@ export function telemetryHandler(req: any, res: any): void {
|
|
|
269
271
|
res.status(500).json({ success: false, error: 'Failed to write log' });
|
|
270
272
|
}
|
|
271
273
|
};
|
|
272
|
-
|
|
274
|
+
|
|
273
275
|
handler();
|
|
274
276
|
}
|
|
275
277
|
|
package/src/api/route.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Server-only exports for @jhits/plugin-telemetry
|
|
3
5
|
* These exports should only be used in server-side code (API routes, server components)
|
|
@@ -80,10 +82,10 @@ function formatLogEntry(entry: TelemetryEntry, debug: boolean = false): string {
|
|
|
80
82
|
const context = entry.context || 'unknown';
|
|
81
83
|
const userId = entry.userId || 'anonymous';
|
|
82
84
|
const sessionId = entry.sessionId || 'unknown';
|
|
83
|
-
|
|
85
|
+
|
|
84
86
|
// Check for debug mode via environment variable
|
|
85
87
|
const isDebugMode = debug || process.env.TELEMETRY_DEBUG === 'true';
|
|
86
|
-
|
|
88
|
+
|
|
87
89
|
if (isDebugMode) {
|
|
88
90
|
// Pretty-printed format for debugging
|
|
89
91
|
const logObject = {
|
|
@@ -99,12 +101,12 @@ function formatLogEntry(entry: TelemetryEntry, debug: boolean = false): string {
|
|
|
99
101
|
return JSON.stringify(logObject, null, 2) + '\n';
|
|
100
102
|
} else {
|
|
101
103
|
// Single-line JSON format for production (easier grep/tailing)
|
|
102
|
-
const sanitizedData = typeof entry.data === 'object'
|
|
104
|
+
const sanitizedData = typeof entry.data === 'object'
|
|
103
105
|
? JSON.stringify(entry.data).replace(/\n/g, '\\n').replace(/\r/g, '\\r')
|
|
104
106
|
: String(entry.data);
|
|
105
|
-
|
|
107
|
+
|
|
106
108
|
const stackStr = entry.stack ? ` "stack":"${entry.stack.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"` : '';
|
|
107
|
-
|
|
109
|
+
|
|
108
110
|
return `[${timestamp}] [${context}] [${userId}] [${sessionId}] [${entry.category}] ${entry.message} ${sanitizedData}${stackStr}\n`;
|
|
109
111
|
}
|
|
110
112
|
}
|
|
@@ -149,16 +151,16 @@ function validateEntry(entry: any): TelemetryEntry | null {
|
|
|
149
151
|
*/
|
|
150
152
|
function extractContext(request: NextRequest): { userId?: string; sessionId?: string } {
|
|
151
153
|
// Try to get user ID from headers (custom header)
|
|
152
|
-
const userId = request.headers.get('x-user-id') ||
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
const userId = request.headers.get('x-user-id') ||
|
|
155
|
+
request.headers.get('x-userid') ||
|
|
156
|
+
undefined;
|
|
157
|
+
|
|
156
158
|
// Try to get session ID from headers or cookies
|
|
157
159
|
const sessionId = request.headers.get('x-session-id') ||
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
request.headers.get('x-sessionid') ||
|
|
161
|
+
request.cookies.get('sessionId')?.value ||
|
|
162
|
+
request.cookies.get('session')?.value ||
|
|
163
|
+
undefined;
|
|
162
164
|
|
|
163
165
|
return { userId, sessionId };
|
|
164
166
|
}
|
|
@@ -178,13 +180,13 @@ export async function telemetryHandler(
|
|
|
178
180
|
try {
|
|
179
181
|
// Ensure log directory exists
|
|
180
182
|
await ensureLogDir();
|
|
181
|
-
|
|
183
|
+
|
|
182
184
|
// Rotate log if needed
|
|
183
185
|
await rotateLogIfNeeded();
|
|
184
|
-
|
|
186
|
+
|
|
185
187
|
// Handle single entry or batch of entries
|
|
186
188
|
const entries = Array.isArray(data) ? data : [data];
|
|
187
|
-
|
|
189
|
+
|
|
188
190
|
// Rate limiting: Check batch size
|
|
189
191
|
if (entries.length > MAX_BATCH_SIZE) {
|
|
190
192
|
return {
|
|
@@ -192,7 +194,7 @@ export async function telemetryHandler(
|
|
|
192
194
|
error: `Batch size exceeds maximum of ${MAX_BATCH_SIZE}`
|
|
193
195
|
};
|
|
194
196
|
}
|
|
195
|
-
|
|
197
|
+
|
|
196
198
|
// Validate and sanitize entries
|
|
197
199
|
const validEntries: TelemetryEntry[] = [];
|
|
198
200
|
for (const entry of entries) {
|
|
@@ -208,19 +210,19 @@ export async function telemetryHandler(
|
|
|
208
210
|
validEntries.push(validated);
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
|
-
|
|
213
|
+
|
|
212
214
|
if (validEntries.length === 0) {
|
|
213
215
|
return {
|
|
214
216
|
success: false,
|
|
215
217
|
error: 'No valid entries to log'
|
|
216
218
|
};
|
|
217
219
|
}
|
|
218
|
-
|
|
220
|
+
|
|
219
221
|
// Format and append each entry (check for debug mode)
|
|
220
222
|
const debugMode = process.env.TELEMETRY_DEBUG === 'true';
|
|
221
223
|
const logLines = validEntries.map(entry => formatLogEntry(entry, debugMode)).join('');
|
|
222
224
|
await fs.appendFile(LOG_FILE, logLines, 'utf8');
|
|
223
|
-
|
|
225
|
+
|
|
224
226
|
return {
|
|
225
227
|
success: true,
|
|
226
228
|
logged: validEntries.length,
|