@qiaolei81/copilot-session-viewer 0.3.2 → 0.3.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/src/app.js CHANGED
@@ -9,7 +9,7 @@ const config = require('./config');
9
9
  // Middleware
10
10
  // Rate limiting disabled for local development
11
11
  // const { globalLimiter, insightGenerationLimiter, insightAccessLimiter, uploadLimiter } = require('./middleware/rateLimiting');
12
- const { requestTimeout, developmentCors, errorHandler, notFoundHandler } = require('./middleware/common');
12
+ const { requestTimeout, developmentCors, errorHandler, notFoundHandler, telemetryLocals } = require('./middleware/common');
13
13
 
14
14
  // Controllers
15
15
  const SessionController = require('./controllers/sessionController');
@@ -68,6 +68,7 @@ function createApp(options = {}) {
68
68
  app.use(express.json({ limit: '1mb' }));
69
69
  app.use(express.urlencoded({ extended: true }));
70
70
  app.use(requestTimeout);
71
+ app.use(telemetryLocals);
71
72
 
72
73
  // CORS in development
73
74
  if (config.NODE_ENV === 'development') {
@@ -1,5 +1,6 @@
1
1
  const InsightService = require('../services/insightService');
2
2
  const { isValidSessionId } = require('../utils/helpers');
3
+ const { trackEvent, trackMetric, trackException } = require('../telemetry');
3
4
 
4
5
  class InsightController {
5
6
  constructor(insightService = null, sessionService = null) {
@@ -39,10 +40,30 @@ class InsightController {
39
40
  return res.status(400).json({ error: 'Session directory not available' });
40
41
  }
41
42
 
43
+ const startTime = Date.now();
42
44
  const result = await this.insightService.generateInsight(session.id, session.directory, session.source, forceRegenerate);
45
+ const durationMs = Date.now() - startTime;
46
+
47
+ // Track InsightGenerated event
48
+ trackEvent('InsightGenerated', {
49
+ sessionId,
50
+ source: session.source || 'unknown',
51
+ durationMs: durationMs.toString()
52
+ });
53
+
54
+ // Track InsightGenerationTime metric
55
+ trackMetric('InsightGenerationTime', durationMs, { sessionId, source: session.source || 'unknown' });
56
+
43
57
  res.json(result);
44
58
  } catch (err) {
45
59
  console.error('Error generating insight:', err);
60
+
61
+ // Track insight generation failure
62
+ trackException(err, {
63
+ sessionId: req.params.id,
64
+ operation: 'generateInsight'
65
+ });
66
+
46
67
  res.status(500).json({ error: err.message || 'Error generating insight' });
47
68
  }
48
69
  }
@@ -67,6 +88,12 @@ class InsightController {
67
88
  }
68
89
 
69
90
  const result = await this.insightService.getInsightStatus(session.id, session.directory, session.source);
91
+
92
+ // Track InsightViewed event if insight is ready
93
+ if (result.status === 'ready' && result.report) {
94
+ trackEvent('InsightViewed', { sessionId });
95
+ }
96
+
70
97
  res.json(result);
71
98
  } catch (err) {
72
99
  console.error('Error getting insight status:', err);
@@ -94,6 +121,10 @@ class InsightController {
94
121
  }
95
122
 
96
123
  const result = await this.insightService.deleteInsight(session.id, session.directory, session.source);
124
+
125
+ // Track InsightDeleted event
126
+ trackEvent('InsightDeleted', { sessionId });
127
+
97
128
  res.json(result);
98
129
  } catch (err) {
99
130
  console.error('Error deleting insight:', err);
@@ -1,5 +1,6 @@
1
1
  const SessionService = require('../services/sessionService');
2
2
  const { isValidSessionId, buildMetadata } = require('../utils/helpers');
3
+ const { trackEvent, trackMetric } = require('../telemetry');
3
4
  const AdmZip = require('adm-zip');
4
5
  const path = require('path');
5
6
  const fs = require('fs');
@@ -40,6 +41,12 @@ class SessionController {
40
41
  sourceHints: JSON.stringify(sourceHints)
41
42
  };
42
43
 
44
+ // Track HomepageViewed event
45
+ trackEvent('HomepageViewed', {
46
+ sessionCount: paginationData.totalSessions.toString(),
47
+ sourceFilter: 'copilot'
48
+ });
49
+
43
50
  res.render('index', templateData);
44
51
  } catch (err) {
45
52
  console.error('Error loading sessions:', err);
@@ -62,6 +69,29 @@ class SessionController {
62
69
  }
63
70
 
64
71
  const metadata = buildMetadata(session);
72
+
73
+ // Track SessionViewed event
74
+ trackEvent('SessionViewed', {
75
+ sessionId,
76
+ source: session.source || 'unknown',
77
+ eventCount: (session.eventCount || metadata.totalEvents || 0).toString(),
78
+ duration: (session.duration || metadata.duration || 0).toString(),
79
+ model: session.model || metadata.model || 'unknown',
80
+ sessionStatus: session.status || metadata.status || 'unknown'
81
+ });
82
+
83
+ // Track SessionEventCount metric
84
+ const eventCount = session.eventCount || metadata.totalEvents || 0;
85
+ if (eventCount > 0) {
86
+ trackMetric('SessionEventCount', eventCount, { sessionId, source: session.source || 'unknown' });
87
+ }
88
+
89
+ // Track SessionDuration metric
90
+ const duration = session.duration || metadata.duration || 0;
91
+ if (duration > 0) {
92
+ trackMetric('SessionDuration', duration, { sessionId, source: session.source || 'unknown' });
93
+ }
94
+
65
95
  res.render('session-vue', { sessionId, events: [], metadata });
66
96
  } catch (err) {
67
97
  console.error('Error loading session:', err);
@@ -84,6 +114,13 @@ class SessionController {
84
114
  }
85
115
 
86
116
  const metadata = buildMetadata(session);
117
+
118
+ // Track TimeAnalysisViewed event
119
+ trackEvent('TimeAnalysisViewed', {
120
+ sessionId,
121
+ turnCount: (metadata.totalEvents || 0).toString()
122
+ });
123
+
87
124
  res.render('time-analyze', { sessionId, events: [], metadata });
88
125
  } catch (err) {
89
126
  console.error('Error loading time analysis:', err);
@@ -104,6 +141,14 @@ class SessionController {
104
141
  return res.status(400).json({ error: 'Invalid pagination parameters' });
105
142
  }
106
143
  const paginationData = await this.sessionService.getPaginatedSessions(page, limit, sourceFilter);
144
+
145
+ // Track SessionListLoaded event for API pagination
146
+ trackEvent('SessionListLoaded', {
147
+ page: page.toString(),
148
+ limit: limit.toString(),
149
+ totalSessions: paginationData.totalSessions.toString()
150
+ });
151
+
107
152
  res.set({ 'Cache-Control': 'public, max-age=60' });
108
153
  res.json(paginationData);
109
154
  } else if (sourceFilter && limit) {
@@ -140,6 +185,13 @@ class SessionController {
140
185
  const page = Math.floor(offset / limit) + 1;
141
186
  const paginationData = await this.sessionService.getPaginatedSessions(page, limit, sourceFilter);
142
187
 
188
+ // Track SessionListLoaded event
189
+ trackEvent('SessionListLoaded', {
190
+ page: page.toString(),
191
+ limit: limit.toString(),
192
+ totalSessions: paginationData.totalSessions.toString()
193
+ });
194
+
143
195
  res.json({
144
196
  sessions: paginationData.sessions,
145
197
  hasMore: paginationData.hasNextPage,
@@ -382,6 +434,10 @@ class SessionController {
382
434
  const zipBuffer = zip.toBuffer();
383
435
  res.setHeader('Content-Type', 'application/zip');
384
436
  res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.zip"`);
437
+
438
+ // Track SessionExported event
439
+ trackEvent('SessionExported', { sessionId });
440
+
385
441
  res.send(zipBuffer);
386
442
  } catch (err) {
387
443
  console.error('Error exporting session:', err);
@@ -1,6 +1,7 @@
1
1
  const TagService = require('../services/tagService');
2
2
  const SessionRepository = require('../services/sessionRepository');
3
3
  const { isValidSessionId } = require('../utils/helpers');
4
+ const { trackEvent } = require('../telemetry');
4
5
 
5
6
  class TagController {
6
7
  constructor(tagService = null, sessionRepository = null) {
@@ -88,6 +89,13 @@ class TagController {
88
89
  }
89
90
 
90
91
  const savedTags = await this.tagService.setSessionTags(session, tags);
92
+
93
+ // Track TagUpdated event
94
+ trackEvent('TagUpdated', {
95
+ sessionId,
96
+ tagCount: savedTags.length.toString()
97
+ });
98
+
91
99
  res.json({ tags: savedTags });
92
100
  } catch (err) {
93
101
  console.error('Error setting session tags:', err);
@@ -4,6 +4,7 @@ const os = require('os');
4
4
  const multer = require('multer');
5
5
  const { spawn } = require('child_process');
6
6
  const { isValidSessionId } = require('../utils/helpers');
7
+ const { trackEvent, trackException } = require('../telemetry');
7
8
  const processManager = require('../utils/processManager');
8
9
  const config = require('../config');
9
10
 
@@ -72,6 +73,9 @@ class UploadController {
72
73
  return res.status(500).json({ error: 'Failed to create zip file' });
73
74
  }
74
75
 
76
+ // Track SessionShared event
77
+ trackEvent('SessionShared', { sessionId });
78
+
75
79
  res.download(zipFile, `session-${sessionId}.zip`, (err) => {
76
80
  fs.promises.unlink(zipFile).catch(() => {});
77
81
  if (err) {
@@ -181,6 +185,7 @@ class UploadController {
181
185
  processManager.register(unzipProcess, { name: 'unzip-import' });
182
186
 
183
187
  unzipProcess.on('close', async (code) => {
188
+ let sessionDirName; // Declare here for access in catch block
184
189
  try {
185
190
  await fs.promises.unlink(zipPath);
186
191
 
@@ -195,7 +200,7 @@ class UploadController {
195
200
  return res.status(400).json({ error: 'Empty zip file' });
196
201
  }
197
202
 
198
- const sessionDirName = entries[0];
203
+ sessionDirName = entries[0];
199
204
 
200
205
  // Validate session directory name to prevent Zip Slip path traversal
201
206
  if (!isValidSessionId(sessionDirName)) {
@@ -222,9 +227,23 @@ class UploadController {
222
227
  await fs.promises.rename(sessionPath, targetPath);
223
228
  await fs.promises.rm(extractDir, { recursive: true, force: true });
224
229
 
230
+ // Track SessionImported event
231
+ const stats = await fs.promises.stat(zipPath).catch(() => ({ size: 0 }));
232
+ trackEvent('SessionImported', {
233
+ format: 'copilot',
234
+ fileSize: stats.size.toString()
235
+ });
236
+
225
237
  res.json({ success: true, sessionId: sessionDirName });
226
238
  } catch (err) {
227
239
  console.error('Error importing session:', err);
240
+
241
+ // Track import failure
242
+ trackException(err, {
243
+ operation: 'importSession',
244
+ sessionId: sessionDirName || 'unknown'
245
+ });
246
+
228
247
  await fs.promises.rm(extractDir, { recursive: true, force: true }).catch(() => {});
229
248
  res.status(500).json({ error: 'Error importing session' });
230
249
  }
@@ -232,12 +251,24 @@ class UploadController {
232
251
 
233
252
  unzipProcess.on('error', async (err) => {
234
253
  console.error('Error extracting zip:', err);
254
+
255
+ // Track upload/extraction failure
256
+ trackException(err, {
257
+ operation: 'importSession_unzip'
258
+ });
259
+
235
260
  await fs.promises.unlink(zipPath).catch(() => {});
236
261
  await fs.promises.rm(extractDir, { recursive: true, force: true }).catch(() => {});
237
262
  res.status(500).json({ error: 'Failed to extract zip file' });
238
263
  });
239
264
  } catch (err) {
240
265
  console.error('Error processing upload:', err);
266
+
267
+ // Track upload processing failure
268
+ trackException(err, {
269
+ operation: 'importSession_upload'
270
+ });
271
+
241
272
  if (req.file) {
242
273
  await fs.promises.unlink(req.file.path).catch(() => {});
243
274
  }
@@ -1,4 +1,5 @@
1
1
  const config = require('../config');
2
+ const { trackException, isEnabled: isTelemetryEnabled } = require('../telemetry');
2
3
 
3
4
  // Request timeout middleware
4
5
  const requestTimeout = (req, res, next) => {
@@ -6,6 +7,15 @@ const requestTimeout = (req, res, next) => {
6
7
  next();
7
8
  };
8
9
 
10
+ // Telemetry middleware - makes telemetry settings available to templates
11
+ const telemetryLocals = (req, res, next) => {
12
+ res.locals.telemetryEnabled = isTelemetryEnabled;
13
+ res.locals.telemetryConnectionString = isTelemetryEnabled
14
+ ? (process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || 'InstrumentationKey=39f4fbf1-d82f-42c3-b4ef-ea92a1fd82cb;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=7d4bb432-f2f5-4526-a5e6-31901e5a2db2')
15
+ : null;
16
+ next();
17
+ };
18
+
9
19
  // CORS middleware for development
10
20
  const developmentCors = (req, res, next) => {
11
21
  if (config.NODE_ENV === 'development') {
@@ -24,6 +34,14 @@ const developmentCors = (req, res, next) => {
24
34
  const errorHandler = (err, req, res, _next) => {
25
35
  console.error('Unhandled error:', err.stack);
26
36
 
37
+ // Track exception in Application Insights
38
+ trackException(err, {
39
+ url: req.url,
40
+ method: req.method,
41
+ statusCode: (err.status || 500).toString(),
42
+ userAgent: (req.headers && req.headers['user-agent']) || 'unknown'
43
+ });
44
+
27
45
  const statusCode = err.status || 500;
28
46
  // Default to production-safe behavior if NODE_ENV is not set
29
47
  const isDevelopment = config.NODE_ENV === 'development';
@@ -44,5 +62,6 @@ module.exports = {
44
62
  requestTimeout,
45
63
  developmentCors,
46
64
  errorHandler,
47
- notFoundHandler
65
+ notFoundHandler,
66
+ telemetryLocals
48
67
  };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Application Insights Telemetry Module
3
+ *
4
+ * This module initializes and configures Application Insights for telemetry tracking.
5
+ * Must be required BEFORE any other modules (especially Express) in server.js.
6
+ *
7
+ * Features:
8
+ * - Auto-collection of requests, dependencies, exceptions, and performance counters
9
+ * - Custom event and metric tracking
10
+ * - Automatic disabling in test environments
11
+ * - Support for manual disabling via DISABLE_TELEMETRY env var
12
+ */
13
+
14
+ const appInsights = require('applicationinsights');
15
+
16
+ // Determine if telemetry should be disabled
17
+ const isTestEnvironment = process.env.NODE_ENV === 'test';
18
+ const isDisabled = process.env.DISABLE_TELEMETRY === 'true' || isTestEnvironment;
19
+
20
+ // Default connection string (can be overridden via env var)
21
+ const DEFAULT_CONNECTION_STRING = 'InstrumentationKey=39f4fbf1-d82f-42c3-b4ef-ea92a1fd82cb;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=7d4bb432-f2f5-4526-a5e6-31901e5a2db2';
22
+
23
+ let client = null;
24
+
25
+ if (!isDisabled) {
26
+ try {
27
+ // Get connection string from environment or use default
28
+ const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || DEFAULT_CONNECTION_STRING;
29
+
30
+ // Setup and start Application Insights
31
+ appInsights.setup(connectionString)
32
+ .setAutoDependencyCorrelation(true)
33
+ .setAutoCollectRequests(true)
34
+ .setAutoCollectPerformance(true, true)
35
+ .setAutoCollectExceptions(true)
36
+ .setAutoCollectDependencies(true)
37
+ .setAutoCollectConsole(false) // Disable console tracking to avoid noise
38
+ .setUseDiskRetryCaching(true)
39
+ .setSendLiveMetrics(false) // Disable live metrics for local dev tool
40
+ .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
41
+ .start();
42
+
43
+ client = appInsights.defaultClient;
44
+
45
+ // Set context properties
46
+ client.context.tags[client.context.keys.cloudRole] = 'copilot-session-viewer';
47
+ client.context.tags[client.context.keys.cloudRoleInstance] = require('os').hostname();
48
+
49
+ console.log('✅ Application Insights telemetry initialized');
50
+ } catch (error) {
51
+ console.error('❌ Failed to initialize Application Insights:', error.message);
52
+ // Continue without telemetry rather than crashing
53
+ client = createNoOpClient();
54
+ }
55
+ } else {
56
+ // Return no-op client for test environment or when disabled
57
+ client = createNoOpClient();
58
+
59
+ if (isTestEnvironment) {
60
+ console.log('📊 Telemetry disabled (test environment)');
61
+ } else {
62
+ console.log('📊 Telemetry disabled (DISABLE_TELEMETRY=true)');
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Creates a no-op client that safely ignores all telemetry calls
68
+ * Used when telemetry is disabled or in test environments
69
+ */
70
+ function createNoOpClient() {
71
+ return {
72
+ trackEvent: () => {},
73
+ trackMetric: () => {},
74
+ trackException: () => {},
75
+ trackTrace: () => {},
76
+ trackDependency: () => {},
77
+ trackRequest: () => {},
78
+ flush: (callback) => {
79
+ if (callback) callback();
80
+ }
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Track a custom event
86
+ * @param {string} name - Event name
87
+ * @param {Object} properties - Event properties
88
+ */
89
+ function trackEvent(name, properties = {}) {
90
+ if (client && client.trackEvent) {
91
+ client.trackEvent({
92
+ name,
93
+ properties
94
+ });
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Track a custom metric
100
+ * @param {string} name - Metric name
101
+ * @param {number} value - Metric value
102
+ * @param {Object} properties - Additional properties
103
+ */
104
+ function trackMetric(name, value, properties = {}) {
105
+ if (client && client.trackMetric) {
106
+ client.trackMetric({
107
+ name,
108
+ value,
109
+ properties
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Track an exception
116
+ * @param {Error} error - Error object
117
+ * @param {Object} properties - Additional properties
118
+ */
119
+ function trackException(error, properties = {}) {
120
+ if (client && client.trackException) {
121
+ client.trackException({
122
+ exception: error,
123
+ properties
124
+ });
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Flush telemetry data (useful for short-lived processes)
130
+ * @returns {Promise<void>}
131
+ */
132
+ function flush() {
133
+ return new Promise((resolve) => {
134
+ if (client && client.flush) {
135
+ client.flush({
136
+ callback: () => resolve()
137
+ });
138
+ } else {
139
+ resolve();
140
+ }
141
+ });
142
+ }
143
+
144
+ // Export the client and helper functions
145
+ module.exports = {
146
+ client,
147
+ trackEvent,
148
+ trackMetric,
149
+ trackException,
150
+ flush,
151
+ isEnabled: !isDisabled
152
+ };