@chappibunny/repolens 0.4.3 → 0.6.2
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/CHANGELOG.md +131 -0
- package/README.md +414 -64
- package/package.json +16 -4
- package/src/ai/provider.js +48 -45
- package/src/cli.js +117 -9
- package/src/core/config-schema.js +43 -1
- package/src/core/config.js +20 -3
- package/src/core/scan.js +184 -3
- package/src/init.js +46 -4
- package/src/integrations/discord.js +261 -0
- package/src/migrate.js +7 -0
- package/src/publishers/confluence.js +428 -0
- package/src/publishers/index.js +112 -4
- package/src/publishers/notion.js +20 -16
- package/src/publishers/publish.js +1 -1
- package/src/renderers/render.js +32 -2
- package/src/utils/branch.js +32 -0
- package/src/utils/logger.js +21 -4
- package/src/utils/metrics.js +361 -0
- package/src/utils/rate-limit.js +289 -0
- package/src/utils/secrets.js +240 -0
- package/src/utils/telemetry.js +375 -0
- package/src/utils/validate.js +382 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/node";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { sanitizeSecrets } from "./secrets.js";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
let initialized = false;
|
|
11
|
+
let enabled = false;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initialize error tracking (Sentry)
|
|
15
|
+
* Only enabled if REPOLENS_TELEMETRY_ENABLED=true
|
|
16
|
+
*/
|
|
17
|
+
export function initTelemetry() {
|
|
18
|
+
// Skip if already initialized
|
|
19
|
+
if (initialized) return;
|
|
20
|
+
initialized = true;
|
|
21
|
+
|
|
22
|
+
// Check if telemetry is enabled (opt-in)
|
|
23
|
+
const telemetryEnabled = process.env.REPOLENS_TELEMETRY_ENABLED === "true";
|
|
24
|
+
|
|
25
|
+
if (!telemetryEnabled) {
|
|
26
|
+
enabled = false;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Get version from package.json
|
|
32
|
+
const packageJsonPath = join(__dirname, "../../package.json");
|
|
33
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
34
|
+
const version = packageJson.version || "unknown";
|
|
35
|
+
|
|
36
|
+
Sentry.init({
|
|
37
|
+
dsn: "https://082083dbf5899ed7e65dfd9b8dc72f90@o4511014913703936.ingest.de.sentry.io/4511014919209040", // TODO: Replace with actual DSN
|
|
38
|
+
|
|
39
|
+
// Release tracking
|
|
40
|
+
release: `repolens@${version}`,
|
|
41
|
+
|
|
42
|
+
// Environment
|
|
43
|
+
environment: process.env.NODE_ENV || "production",
|
|
44
|
+
|
|
45
|
+
// Sample rate (10% of errors)
|
|
46
|
+
sampleRate: 0.1,
|
|
47
|
+
|
|
48
|
+
// Only send errors, not all events
|
|
49
|
+
tracesSampleRate: 0,
|
|
50
|
+
|
|
51
|
+
// Privacy: Don't send PII
|
|
52
|
+
beforeSend(event) {
|
|
53
|
+
// Remove potentially sensitive data
|
|
54
|
+
if (event.request) {
|
|
55
|
+
delete event.request.cookies;
|
|
56
|
+
delete event.request.headers;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Remove file paths that might contain usernames
|
|
60
|
+
if (event.exception?.values) {
|
|
61
|
+
event.exception.values = event.exception.values.map(exception => {
|
|
62
|
+
if (exception.stacktrace?.frames) {
|
|
63
|
+
exception.stacktrace.frames = exception.stacktrace.frames.map(frame => {
|
|
64
|
+
if (frame.filename) {
|
|
65
|
+
// Keep only relative paths
|
|
66
|
+
frame.filename = frame.filename.replace(/.*\/RepoLens\//, "");
|
|
67
|
+
}
|
|
68
|
+
return frame;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Sanitize exception messages for secrets
|
|
73
|
+
if (exception.value) {
|
|
74
|
+
exception.value = sanitizeSecrets(exception.value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return exception;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Sanitize event message
|
|
82
|
+
if (event.message) {
|
|
83
|
+
event.message = sanitizeSecrets(event.message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sanitize extra context
|
|
87
|
+
if (event.extra) {
|
|
88
|
+
event.extra = JSON.parse(sanitizeSecrets(JSON.stringify(event.extra)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return event;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
enabled = true;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Silently fail if Sentry init fails - don't break the CLI
|
|
98
|
+
console.error("Failed to initialize telemetry:", error.message);
|
|
99
|
+
enabled = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Capture an error
|
|
105
|
+
*/
|
|
106
|
+
export function captureError(error, context = {}) {
|
|
107
|
+
if (!enabled) return;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
Sentry.captureException(error, {
|
|
111
|
+
extra: context,
|
|
112
|
+
tags: {
|
|
113
|
+
command: context.command || "unknown",
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Silently fail - don't break the CLI if error tracking fails
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Capture a message (non-error event)
|
|
123
|
+
*/
|
|
124
|
+
export function captureMessage(message, level = "info", context = {}) {
|
|
125
|
+
if (!enabled) return;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
Sentry.captureMessage(message, {
|
|
129
|
+
level,
|
|
130
|
+
extra: context,
|
|
131
|
+
tags: {
|
|
132
|
+
command: context.command || "unknown",
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Silently fail
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Add context to current scope
|
|
142
|
+
*/
|
|
143
|
+
export function setContext(key, data) {
|
|
144
|
+
if (!enabled) return;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
Sentry.setContext(key, data);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
// Silently fail
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Flush pending events and close Sentry
|
|
155
|
+
*/
|
|
156
|
+
export async function closeTelemetry() {
|
|
157
|
+
if (!enabled) return;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await Sentry.close(2000); // 2 second timeout
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// Silently fail
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if telemetry is enabled
|
|
168
|
+
*/
|
|
169
|
+
export function isTelemetryEnabled() {
|
|
170
|
+
return enabled;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// Usage Tracking & Observability
|
|
175
|
+
// ============================================================
|
|
176
|
+
|
|
177
|
+
const performanceTimers = new Map();
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Start a performance timer for an operation
|
|
181
|
+
* @param {string} operation - Operation name (e.g., "scan", "render", "publish")
|
|
182
|
+
* @param {object} metadata - Additional context about the operation
|
|
183
|
+
*/
|
|
184
|
+
export function startTimer(operation, metadata = {}) {
|
|
185
|
+
if (!enabled) return;
|
|
186
|
+
|
|
187
|
+
const key = `${operation}_${Date.now()}`;
|
|
188
|
+
performanceTimers.set(key, {
|
|
189
|
+
operation,
|
|
190
|
+
startTime: Date.now(),
|
|
191
|
+
metadata,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return key; // Return key so caller can stop this specific timer
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Stop a performance timer and record the metric
|
|
199
|
+
* @param {string} timerKey - Key returned from startTimer()
|
|
200
|
+
*/
|
|
201
|
+
export function stopTimer(timerKey) {
|
|
202
|
+
if (!enabled || !timerKey) return;
|
|
203
|
+
|
|
204
|
+
const timer = performanceTimers.get(timerKey);
|
|
205
|
+
if (!timer) return;
|
|
206
|
+
|
|
207
|
+
const duration = Date.now() - timer.startTime;
|
|
208
|
+
performanceTimers.delete(timerKey);
|
|
209
|
+
|
|
210
|
+
// Send performance metric
|
|
211
|
+
try {
|
|
212
|
+
Sentry.metrics.distribution(
|
|
213
|
+
`operation.duration`,
|
|
214
|
+
duration,
|
|
215
|
+
{
|
|
216
|
+
unit: 'millisecond',
|
|
217
|
+
tags: {
|
|
218
|
+
operation: timer.operation,
|
|
219
|
+
...timer.metadata,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
// Silently fail
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return duration;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Track a usage event (command execution)
|
|
232
|
+
* @param {string} command - Command name (init, doctor, migrate, publish)
|
|
233
|
+
* @param {string} status - "success" or "failure"
|
|
234
|
+
* @param {object} metrics - Metrics about the operation
|
|
235
|
+
*/
|
|
236
|
+
export function trackUsage(command, status, metrics = {}) {
|
|
237
|
+
if (!enabled) return;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
// Anonymize repository info
|
|
241
|
+
const sanitizedMetrics = {
|
|
242
|
+
// Command info
|
|
243
|
+
command,
|
|
244
|
+
status,
|
|
245
|
+
|
|
246
|
+
// Repository metrics (sanitized)
|
|
247
|
+
fileCount: metrics.fileCount || 0,
|
|
248
|
+
moduleCount: metrics.moduleCount || 0,
|
|
249
|
+
|
|
250
|
+
// AI usage
|
|
251
|
+
aiEnabled: Boolean(metrics.aiEnabled),
|
|
252
|
+
aiProvider: metrics.aiProvider || null,
|
|
253
|
+
|
|
254
|
+
// Publishers used
|
|
255
|
+
publishers: metrics.publishers || [],
|
|
256
|
+
|
|
257
|
+
// Performance
|
|
258
|
+
duration: metrics.duration || null,
|
|
259
|
+
|
|
260
|
+
// Environment (no sensitive data)
|
|
261
|
+
nodeVersion: process.version,
|
|
262
|
+
platform: process.platform,
|
|
263
|
+
|
|
264
|
+
// Timestamp
|
|
265
|
+
timestamp: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Send as custom Sentry event
|
|
269
|
+
Sentry.captureMessage(`Command: ${command}`, {
|
|
270
|
+
level: status === "success" ? "info" : "warning",
|
|
271
|
+
tags: {
|
|
272
|
+
command,
|
|
273
|
+
status,
|
|
274
|
+
aiEnabled: String(sanitizedMetrics.aiEnabled),
|
|
275
|
+
},
|
|
276
|
+
extra: sanitizedMetrics,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Also track as metric for aggregation
|
|
280
|
+
Sentry.metrics.increment('command.executed', 1, {
|
|
281
|
+
tags: {
|
|
282
|
+
command,
|
|
283
|
+
status,
|
|
284
|
+
ai_enabled: String(sanitizedMetrics.aiEnabled),
|
|
285
|
+
platform: process.platform,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
} catch (e) {
|
|
290
|
+
// Silently fail
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Track scan metrics
|
|
296
|
+
* @param {object} scanResult - Result from scanRepo()
|
|
297
|
+
*/
|
|
298
|
+
export function trackScan(scanResult) {
|
|
299
|
+
if (!enabled) return;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const metrics = {
|
|
303
|
+
filesCount: scanResult.filesCount || 0,
|
|
304
|
+
modulesCount: scanResult.modules?.length || 0,
|
|
305
|
+
apiEndpointsCount: scanResult.api?.length || 0,
|
|
306
|
+
pagesCount: scanResult.pages?.length || 0,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Record metrics
|
|
310
|
+
Sentry.metrics.gauge('scan.files', metrics.filesCount);
|
|
311
|
+
Sentry.metrics.gauge('scan.modules', metrics.modulesCount);
|
|
312
|
+
Sentry.metrics.gauge('scan.api_endpoints', metrics.apiEndpointsCount);
|
|
313
|
+
Sentry.metrics.gauge('scan.pages', metrics.pagesCount);
|
|
314
|
+
|
|
315
|
+
} catch (e) {
|
|
316
|
+
// Silently fail
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Track document generation
|
|
322
|
+
* @param {number} documentCount - Number of documents generated
|
|
323
|
+
* @param {boolean} aiEnabled - Whether AI was used
|
|
324
|
+
*/
|
|
325
|
+
export function trackDocumentGeneration(documentCount, aiEnabled) {
|
|
326
|
+
if (!enabled) return;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
Sentry.metrics.gauge('docs.generated', documentCount, {
|
|
330
|
+
tags: {
|
|
331
|
+
ai_enabled: String(aiEnabled),
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
} catch (e) {
|
|
335
|
+
// Silently fail
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Track publishing
|
|
341
|
+
* @param {string[]} publishers - List of publishers used (e.g., ["notion", "markdown"])
|
|
342
|
+
* @param {string} status - "success" or "failure"
|
|
343
|
+
*/
|
|
344
|
+
export function trackPublishing(publishers, status) {
|
|
345
|
+
if (!enabled) return;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
publishers.forEach(publisher => {
|
|
349
|
+
Sentry.metrics.increment('publish.attempt', 1, {
|
|
350
|
+
tags: {
|
|
351
|
+
publisher,
|
|
352
|
+
status,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
} catch (e) {
|
|
357
|
+
// Silently fail
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Track migration
|
|
363
|
+
* @param {number} migratedCount - Number of workflows migrated
|
|
364
|
+
* @param {number} skippedCount - Number of workflows skipped
|
|
365
|
+
*/
|
|
366
|
+
export function trackMigration(migratedCount, skippedCount) {
|
|
367
|
+
if (!enabled) return;
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
Sentry.metrics.gauge('migration.workflows_migrated', migratedCount);
|
|
371
|
+
Sentry.metrics.gauge('migration.workflows_skipped', skippedCount);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
// Silently fail
|
|
374
|
+
}
|
|
375
|
+
}
|