@dependabit/action 0.1.1
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 +12 -0
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/action.yml +85 -0
- package/dist/actions/check.d.ts +33 -0
- package/dist/actions/check.d.ts.map +1 -0
- package/dist/actions/check.js +162 -0
- package/dist/actions/check.js.map +1 -0
- package/dist/actions/generate.d.ts +9 -0
- package/dist/actions/generate.d.ts.map +1 -0
- package/dist/actions/generate.js +152 -0
- package/dist/actions/generate.js.map +1 -0
- package/dist/actions/update.d.ts +9 -0
- package/dist/actions/update.d.ts.map +1 -0
- package/dist/actions/update.js +246 -0
- package/dist/actions/update.js.map +1 -0
- package/dist/actions/validate.d.ts +33 -0
- package/dist/actions/validate.d.ts.map +1 -0
- package/dist/actions/validate.js +226 -0
- package/dist/actions/validate.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +114 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +154 -0
- package/dist/logger.js.map +1 -0
- package/dist/utils/agent-config.d.ts +31 -0
- package/dist/utils/agent-config.d.ts.map +1 -0
- package/dist/utils/agent-config.js +42 -0
- package/dist/utils/agent-config.js.map +1 -0
- package/dist/utils/agent-router.d.ts +33 -0
- package/dist/utils/agent-router.d.ts.map +1 -0
- package/dist/utils/agent-router.js +57 -0
- package/dist/utils/agent-router.js.map +1 -0
- package/dist/utils/errors.d.ts +51 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +219 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/inputs.d.ts +35 -0
- package/dist/utils/inputs.d.ts.map +1 -0
- package/dist/utils/inputs.js +47 -0
- package/dist/utils/inputs.js.map +1 -0
- package/dist/utils/metrics.d.ts +66 -0
- package/dist/utils/metrics.d.ts.map +1 -0
- package/dist/utils/metrics.js +116 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/utils/outputs.d.ts +43 -0
- package/dist/utils/outputs.d.ts.map +1 -0
- package/dist/utils/outputs.js +146 -0
- package/dist/utils/outputs.js.map +1 -0
- package/dist/utils/performance.d.ts +100 -0
- package/dist/utils/performance.d.ts.map +1 -0
- package/dist/utils/performance.js +185 -0
- package/dist/utils/performance.js.map +1 -0
- package/dist/utils/reporter.d.ts +43 -0
- package/dist/utils/reporter.d.ts.map +1 -0
- package/dist/utils/reporter.js +122 -0
- package/dist/utils/reporter.js.map +1 -0
- package/dist/utils/secrets.d.ts +45 -0
- package/dist/utils/secrets.d.ts.map +1 -0
- package/dist/utils/secrets.js +94 -0
- package/dist/utils/secrets.js.map +1 -0
- package/package.json +45 -0
- package/src/actions/check.ts +223 -0
- package/src/actions/generate.ts +181 -0
- package/src/actions/update.ts +284 -0
- package/src/actions/validate.ts +292 -0
- package/src/index.ts +43 -0
- package/src/logger.test.ts +200 -0
- package/src/logger.ts +210 -0
- package/src/utils/agent-config.ts +61 -0
- package/src/utils/agent-router.ts +67 -0
- package/src/utils/errors.ts +251 -0
- package/src/utils/inputs.ts +75 -0
- package/src/utils/metrics.ts +169 -0
- package/src/utils/outputs.ts +202 -0
- package/src/utils/performance.ts +248 -0
- package/src/utils/reporter.ts +169 -0
- package/src/utils/secrets.ts +124 -0
- package/test/actions/check.test.ts +216 -0
- package/test/actions/generate.test.ts +82 -0
- package/test/actions/update.test.ts +70 -0
- package/test/actions/validate.test.ts +257 -0
- package/test/utils/agent-config.test.ts +112 -0
- package/test/utils/agent-router.test.ts +129 -0
- package/test/utils/metrics.test.ts +221 -0
- package/test/utils/reporter.test.ts +196 -0
- package/test/utils/secrets.test.ts +217 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics calculator for false positive rate tracking
|
|
3
|
+
* Implements 30-day rolling window analysis and trend detection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface MetricsConfig {
|
|
7
|
+
windowDays?: number;
|
|
8
|
+
threshold?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FeedbackSummary {
|
|
12
|
+
truePositives: number;
|
|
13
|
+
falsePositives: number;
|
|
14
|
+
total: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DataPoint {
|
|
18
|
+
date: string;
|
|
19
|
+
falsePositiveRate: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Trend {
|
|
23
|
+
direction: 'improving' | 'worsening' | 'stable' | 'no-data';
|
|
24
|
+
slope: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ThresholdCheck {
|
|
28
|
+
passed: boolean;
|
|
29
|
+
rate: number;
|
|
30
|
+
threshold: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MetricsReport {
|
|
34
|
+
currentRate: number;
|
|
35
|
+
rollingAverage: number;
|
|
36
|
+
trend: Trend;
|
|
37
|
+
thresholdCheck: ThresholdCheck;
|
|
38
|
+
totalFeedback: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculator for false positive metrics with rolling window analysis
|
|
43
|
+
*/
|
|
44
|
+
export class MetricsCalculator {
|
|
45
|
+
private windowDays: number;
|
|
46
|
+
private threshold: number;
|
|
47
|
+
|
|
48
|
+
constructor(config: MetricsConfig = {}) {
|
|
49
|
+
this.windowDays = config.windowDays || 30;
|
|
50
|
+
this.threshold = config.threshold || 0.1; // 10% default threshold
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate false positive rate from feedback data
|
|
55
|
+
*/
|
|
56
|
+
calculateFalsePositiveRate(feedback: FeedbackSummary): number {
|
|
57
|
+
if (feedback.total === 0) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
return feedback.falsePositives / feedback.total;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calculate rolling average over the time window
|
|
65
|
+
*/
|
|
66
|
+
calculateRollingAverage(
|
|
67
|
+
dataPoints: DataPoint[],
|
|
68
|
+
windowDays?: number,
|
|
69
|
+
referenceDate?: Date
|
|
70
|
+
): number {
|
|
71
|
+
const days = windowDays || this.windowDays;
|
|
72
|
+
const endDate = referenceDate || new Date();
|
|
73
|
+
const startDate = new Date(endDate);
|
|
74
|
+
startDate.setDate(startDate.getDate() - days);
|
|
75
|
+
|
|
76
|
+
// Filter data points within the window
|
|
77
|
+
const windowData = dataPoints.filter((point) => {
|
|
78
|
+
const pointDate = new Date(point.date);
|
|
79
|
+
return pointDate >= startDate && pointDate <= endDate;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (windowData.length === 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sum = windowData.reduce((acc, point) => acc + point.falsePositiveRate, 0);
|
|
87
|
+
return sum / windowData.length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Detect trend direction using linear regression
|
|
92
|
+
*/
|
|
93
|
+
getTrend(dataPoints: DataPoint[]): Trend {
|
|
94
|
+
if (dataPoints.length < 2) {
|
|
95
|
+
return { direction: 'no-data', slope: 0 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Simple linear regression
|
|
99
|
+
const n = dataPoints.length;
|
|
100
|
+
let sumX = 0;
|
|
101
|
+
let sumY = 0;
|
|
102
|
+
let sumXY = 0;
|
|
103
|
+
let sumX2 = 0;
|
|
104
|
+
|
|
105
|
+
dataPoints.forEach((point, index) => {
|
|
106
|
+
const x = index;
|
|
107
|
+
const y = point.falsePositiveRate;
|
|
108
|
+
sumX += x;
|
|
109
|
+
sumY += y;
|
|
110
|
+
sumXY += x * y;
|
|
111
|
+
sumX2 += x * x;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
115
|
+
if (denominator === 0) {
|
|
116
|
+
return { direction: 'no-data', slope: 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const slope = (n * sumXY - sumX * sumY) / denominator;
|
|
120
|
+
// Determine direction based on slope
|
|
121
|
+
let direction: Trend['direction'];
|
|
122
|
+
if (Math.abs(slope) < 0.001) {
|
|
123
|
+
direction = 'stable';
|
|
124
|
+
} else if (slope < 0) {
|
|
125
|
+
direction = 'improving';
|
|
126
|
+
} else {
|
|
127
|
+
direction = 'worsening';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { direction, slope };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if rate is within acceptable threshold
|
|
135
|
+
*/
|
|
136
|
+
checkThreshold(rate: number, threshold?: number): ThresholdCheck {
|
|
137
|
+
const targetThreshold = threshold !== undefined ? threshold : this.threshold;
|
|
138
|
+
return {
|
|
139
|
+
passed: rate <= targetThreshold,
|
|
140
|
+
rate,
|
|
141
|
+
threshold: targetThreshold
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Generate comprehensive metrics report
|
|
147
|
+
*/
|
|
148
|
+
generateReport(currentFeedback: FeedbackSummary, history: DataPoint[]): MetricsReport {
|
|
149
|
+
const currentRate = this.calculateFalsePositiveRate(currentFeedback);
|
|
150
|
+
const rollingAverage = this.calculateRollingAverage(history);
|
|
151
|
+
const trend = this.getTrend(history);
|
|
152
|
+
const thresholdCheck = this.checkThreshold(rollingAverage);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
currentRate,
|
|
156
|
+
rollingAverage,
|
|
157
|
+
trend,
|
|
158
|
+
thresholdCheck,
|
|
159
|
+
totalFeedback: currentFeedback.total
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format rate as percentage string
|
|
165
|
+
*/
|
|
166
|
+
formatRate(rate: number): string {
|
|
167
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Output Formatting
|
|
3
|
+
* Handles setting GitHub Action outputs and creating summaries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as core from '@actions/core';
|
|
7
|
+
import type { DependencyManifest } from '@dependabit/manifest';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Set outputs for the generate action
|
|
11
|
+
*/
|
|
12
|
+
export function setGenerateOutputs(
|
|
13
|
+
manifest: DependencyManifest,
|
|
14
|
+
manifestPath: string,
|
|
15
|
+
statistics: {
|
|
16
|
+
filesScanned: number;
|
|
17
|
+
llmCalls: number;
|
|
18
|
+
totalTokens: number;
|
|
19
|
+
totalLatencyMs: number;
|
|
20
|
+
}
|
|
21
|
+
): void {
|
|
22
|
+
core.setOutput('manifest_path', manifestPath);
|
|
23
|
+
core.setOutput('dependency_count', manifest.dependencies.length);
|
|
24
|
+
core.setOutput('files_scanned', statistics.filesScanned);
|
|
25
|
+
core.setOutput('llm_calls', statistics.llmCalls);
|
|
26
|
+
core.setOutput('total_tokens', statistics.totalTokens);
|
|
27
|
+
core.setOutput('average_confidence', manifest.statistics.averageConfidence);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a summary report for the generate action
|
|
32
|
+
*/
|
|
33
|
+
export async function createGenerateSummary(
|
|
34
|
+
manifest: DependencyManifest,
|
|
35
|
+
statistics: {
|
|
36
|
+
filesScanned: number;
|
|
37
|
+
llmCalls: number;
|
|
38
|
+
totalTokens: number;
|
|
39
|
+
totalLatencyMs: number;
|
|
40
|
+
}
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
await core.summary
|
|
43
|
+
.addHeading('🔍 Dependency Detection Complete')
|
|
44
|
+
.addRaw('\n')
|
|
45
|
+
.addTable([
|
|
46
|
+
[
|
|
47
|
+
{ data: 'Metric', header: true },
|
|
48
|
+
{ data: 'Value', header: true }
|
|
49
|
+
],
|
|
50
|
+
['Dependencies Found', manifest.dependencies.length.toString()],
|
|
51
|
+
['Files Scanned', statistics.filesScanned.toString()],
|
|
52
|
+
['LLM Calls', statistics.llmCalls.toString()],
|
|
53
|
+
['Total Tokens Used', statistics.totalTokens.toString()],
|
|
54
|
+
['Average Confidence', `${(manifest.statistics.averageConfidence * 100).toFixed(1)}%`],
|
|
55
|
+
['Analysis Time', `${(statistics.totalLatencyMs / 1000).toFixed(2)}s`]
|
|
56
|
+
])
|
|
57
|
+
.addRaw('\n')
|
|
58
|
+
.addHeading('📊 Dependencies by Type', 3)
|
|
59
|
+
.addList(Object.entries(manifest.statistics.byType).map(([type, count]) => `${type}: ${count}`))
|
|
60
|
+
.addRaw('\n')
|
|
61
|
+
.addHeading('🔧 Dependencies by Access Method', 3)
|
|
62
|
+
.addList(
|
|
63
|
+
Object.entries(manifest.statistics.byAccessMethod).map(
|
|
64
|
+
([method, count]) => `${method}: ${count}`
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
.write();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a summary for detected dependencies
|
|
72
|
+
*/
|
|
73
|
+
export async function createDependencyListSummary(
|
|
74
|
+
dependencies: Array<{ name: string; url: string; type: string; confidence: number }>
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
if (dependencies.length === 0) {
|
|
77
|
+
await core.summary
|
|
78
|
+
.addHeading('No Dependencies Found')
|
|
79
|
+
.addRaw('No external informational dependencies were detected in this repository.')
|
|
80
|
+
.write();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await core.summary
|
|
85
|
+
.addHeading('🔗 Detected Dependencies', 3)
|
|
86
|
+
.addTable([
|
|
87
|
+
[
|
|
88
|
+
{ data: 'Name', header: true },
|
|
89
|
+
{ data: 'Type', header: true },
|
|
90
|
+
{ data: 'Confidence', header: true },
|
|
91
|
+
{ data: 'URL', header: true }
|
|
92
|
+
],
|
|
93
|
+
...dependencies
|
|
94
|
+
.slice(0, 20)
|
|
95
|
+
.map((dep) => [
|
|
96
|
+
dep.name,
|
|
97
|
+
dep.type,
|
|
98
|
+
`${(dep.confidence * 100).toFixed(0)}%`,
|
|
99
|
+
`[Link](${dep.url})`
|
|
100
|
+
])
|
|
101
|
+
])
|
|
102
|
+
.write();
|
|
103
|
+
|
|
104
|
+
if (dependencies.length > 20) {
|
|
105
|
+
await core.summary.addRaw(`\n_...and ${dependencies.length - 20} more dependencies_\n`).write();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Set outputs for the update action
|
|
111
|
+
*/
|
|
112
|
+
function countDependencyDiffs(
|
|
113
|
+
existingManifest: DependencyManifest,
|
|
114
|
+
updatedManifest: DependencyManifest
|
|
115
|
+
): { added: number; removed: number } {
|
|
116
|
+
const existingSet = new Set(existingManifest.dependencies.map((dep) => JSON.stringify(dep)));
|
|
117
|
+
const updatedSet = new Set(updatedManifest.dependencies.map((dep) => JSON.stringify(dep)));
|
|
118
|
+
|
|
119
|
+
let added = 0;
|
|
120
|
+
for (const dep of updatedSet) {
|
|
121
|
+
if (!existingSet.has(dep)) {
|
|
122
|
+
added++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let removed = 0;
|
|
127
|
+
for (const dep of existingSet) {
|
|
128
|
+
if (!updatedSet.has(dep)) {
|
|
129
|
+
removed++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { added, removed };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function setUpdateOutputs(
|
|
137
|
+
updatedManifest: DependencyManifest,
|
|
138
|
+
existingManifest: DependencyManifest,
|
|
139
|
+
filesChanged: number
|
|
140
|
+
): void {
|
|
141
|
+
const { added, removed } = countDependencyDiffs(existingManifest, updatedManifest);
|
|
142
|
+
const changesDetected = added > 0 || removed > 0;
|
|
143
|
+
|
|
144
|
+
core.setOutput('changes_detected', changesDetected);
|
|
145
|
+
core.setOutput('dependencies_added', added);
|
|
146
|
+
core.setOutput('dependencies_removed', removed);
|
|
147
|
+
core.setOutput('total_dependencies', updatedManifest.dependencies.length);
|
|
148
|
+
core.setOutput('files_analyzed', filesChanged);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a summary report for the update action
|
|
153
|
+
*/
|
|
154
|
+
export async function createUpdateSummary(
|
|
155
|
+
existingManifest: DependencyManifest,
|
|
156
|
+
updatedManifest: DependencyManifest,
|
|
157
|
+
stats: {
|
|
158
|
+
commitsAnalyzed: number;
|
|
159
|
+
filesChanged: number;
|
|
160
|
+
urlsAdded: number;
|
|
161
|
+
urlsRemoved: number;
|
|
162
|
+
}
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const dependenciesAdded =
|
|
165
|
+
updatedManifest.dependencies.length - existingManifest.dependencies.length;
|
|
166
|
+
const changesDetected = dependenciesAdded !== 0;
|
|
167
|
+
|
|
168
|
+
await core.summary
|
|
169
|
+
.addHeading('🔄 Manifest Update Complete')
|
|
170
|
+
.addRaw('\n')
|
|
171
|
+
.addTable([
|
|
172
|
+
[
|
|
173
|
+
{ data: 'Metric', header: true },
|
|
174
|
+
{ data: 'Value', header: true }
|
|
175
|
+
],
|
|
176
|
+
['Commits Analyzed', stats.commitsAnalyzed.toString()],
|
|
177
|
+
['Files Changed', stats.filesChanged.toString()],
|
|
178
|
+
['URLs Added', stats.urlsAdded.toString()],
|
|
179
|
+
['URLs Removed', stats.urlsRemoved.toString()],
|
|
180
|
+
['Dependencies Before', existingManifest.dependencies.length.toString()],
|
|
181
|
+
['Dependencies After', updatedManifest.dependencies.length.toString()],
|
|
182
|
+
[
|
|
183
|
+
'Net Change',
|
|
184
|
+
dependenciesAdded >= 0 ? `+${dependenciesAdded}` : dependenciesAdded.toString()
|
|
185
|
+
],
|
|
186
|
+
['Changes Detected', changesDetected ? '✅ Yes' : '❌ No']
|
|
187
|
+
])
|
|
188
|
+
.write();
|
|
189
|
+
|
|
190
|
+
if (dependenciesAdded > 0) {
|
|
191
|
+
const newDeps = updatedManifest.dependencies
|
|
192
|
+
.filter((dep) => !existingManifest.dependencies.some((existing) => existing.url === dep.url))
|
|
193
|
+
.slice(0, 10);
|
|
194
|
+
|
|
195
|
+
if (newDeps.length > 0) {
|
|
196
|
+
await core.summary
|
|
197
|
+
.addHeading('🆕 New Dependencies', 3)
|
|
198
|
+
.addList(newDeps.map((dep) => `**${dep.name}**: ${dep.url} (${dep.type})`))
|
|
199
|
+
.write();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance metrics tracking for operations
|
|
3
|
+
* Tracks operation durations, API quotas, and resource usage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface OperationMetrics {
|
|
7
|
+
operationId?: string | undefined;
|
|
8
|
+
name: string;
|
|
9
|
+
startTime: number;
|
|
10
|
+
endTime?: number | undefined;
|
|
11
|
+
duration?: number | undefined;
|
|
12
|
+
status: 'running' | 'completed' | 'failed';
|
|
13
|
+
metadata?: Record<string, unknown> | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface APIQuotaMetrics {
|
|
17
|
+
limit: number;
|
|
18
|
+
remaining: number;
|
|
19
|
+
used: number;
|
|
20
|
+
resetAt: Date;
|
|
21
|
+
percentUsed: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PerformanceReport {
|
|
25
|
+
totalOperations: number;
|
|
26
|
+
completedOperations: number;
|
|
27
|
+
failedOperations: number;
|
|
28
|
+
averageDuration: number;
|
|
29
|
+
totalDuration: number;
|
|
30
|
+
apiQuota?: APIQuotaMetrics | undefined;
|
|
31
|
+
operations: OperationMetrics[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Performance metrics tracker
|
|
36
|
+
*/
|
|
37
|
+
export class PerformanceTracker {
|
|
38
|
+
private operations: Map<string, OperationMetrics>;
|
|
39
|
+
private completedOperations: OperationMetrics[];
|
|
40
|
+
private apiQuota?: APIQuotaMetrics | undefined;
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
this.operations = new Map();
|
|
44
|
+
this.completedOperations = [];
|
|
45
|
+
this.apiQuota = undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Start tracking an operation
|
|
50
|
+
*/
|
|
51
|
+
startOperation(name: string, metadata?: Record<string, unknown>): string {
|
|
52
|
+
const operationId = `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
53
|
+
|
|
54
|
+
const metrics: OperationMetrics = {
|
|
55
|
+
operationId,
|
|
56
|
+
name,
|
|
57
|
+
startTime: Date.now(),
|
|
58
|
+
status: 'running',
|
|
59
|
+
metadata
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
this.operations.set(operationId, metrics);
|
|
63
|
+
return operationId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* End tracking an operation
|
|
68
|
+
*/
|
|
69
|
+
endOperation(operationId: string, status: 'completed' | 'failed' = 'completed'): void {
|
|
70
|
+
const metrics = this.operations.get(operationId);
|
|
71
|
+
if (!metrics) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
metrics.endTime = Date.now();
|
|
76
|
+
metrics.duration = metrics.endTime - metrics.startTime;
|
|
77
|
+
metrics.status = status;
|
|
78
|
+
|
|
79
|
+
this.completedOperations.push(metrics);
|
|
80
|
+
this.operations.delete(operationId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Update API quota metrics
|
|
85
|
+
*/
|
|
86
|
+
updateAPIQuota(quota: { limit: number; remaining: number; used: number; reset: number }): void {
|
|
87
|
+
this.apiQuota = {
|
|
88
|
+
limit: quota.limit,
|
|
89
|
+
remaining: quota.remaining,
|
|
90
|
+
used: quota.used,
|
|
91
|
+
resetAt: new Date(quota.reset * 1000),
|
|
92
|
+
percentUsed: (quota.used / quota.limit) * 100
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get current API quota status
|
|
98
|
+
*/
|
|
99
|
+
getAPIQuota(): APIQuotaMetrics | undefined {
|
|
100
|
+
return this.apiQuota;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get metrics for a specific operation
|
|
105
|
+
*/
|
|
106
|
+
getOperation(operationId: string): OperationMetrics | undefined {
|
|
107
|
+
return (
|
|
108
|
+
this.operations.get(operationId) ||
|
|
109
|
+
this.completedOperations.find((op) => op.operationId === operationId)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get all running operations
|
|
115
|
+
*/
|
|
116
|
+
getRunningOperations(): OperationMetrics[] {
|
|
117
|
+
return Array.from(this.operations.values());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get all completed operations
|
|
122
|
+
*/
|
|
123
|
+
getCompletedOperations(): OperationMetrics[] {
|
|
124
|
+
return this.completedOperations;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate performance report
|
|
129
|
+
*/
|
|
130
|
+
generateReport(): PerformanceReport {
|
|
131
|
+
const allOperations = this.completedOperations;
|
|
132
|
+
const completed = allOperations.filter((op) => op.status === 'completed');
|
|
133
|
+
const failed = allOperations.filter((op) => op.status === 'failed');
|
|
134
|
+
|
|
135
|
+
const durations = completed.filter((op) => op.duration !== undefined).map((op) => op.duration!);
|
|
136
|
+
|
|
137
|
+
const totalDuration = durations.reduce((sum, d) => sum + d, 0);
|
|
138
|
+
const averageDuration = durations.length > 0 ? totalDuration / durations.length : 0;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
totalOperations: allOperations.length,
|
|
142
|
+
completedOperations: completed.length,
|
|
143
|
+
failedOperations: failed.length,
|
|
144
|
+
averageDuration,
|
|
145
|
+
totalDuration,
|
|
146
|
+
apiQuota: this.apiQuota,
|
|
147
|
+
operations: allOperations
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get operations by name
|
|
153
|
+
*/
|
|
154
|
+
getOperationsByName(name: string): OperationMetrics[] {
|
|
155
|
+
return this.completedOperations.filter((op) => op.name === name);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Calculate percentile duration for an operation
|
|
160
|
+
*/
|
|
161
|
+
getOperationPercentile(name: string, percentile: number): number {
|
|
162
|
+
const operations = this.getOperationsByName(name);
|
|
163
|
+
const durations = operations
|
|
164
|
+
.filter((op) => op.duration !== undefined)
|
|
165
|
+
.map((op) => op.duration!)
|
|
166
|
+
.sort((a, b) => a - b);
|
|
167
|
+
|
|
168
|
+
if (durations.length === 0) {
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const index = Math.ceil((percentile / 100) * durations.length) - 1;
|
|
173
|
+
const value = durations[Math.max(0, index)];
|
|
174
|
+
return value !== undefined ? value : 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Clear all metrics
|
|
179
|
+
*/
|
|
180
|
+
clear(): void {
|
|
181
|
+
this.operations.clear();
|
|
182
|
+
this.completedOperations = [];
|
|
183
|
+
this.apiQuota = undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Format metrics as human-readable string
|
|
188
|
+
*/
|
|
189
|
+
formatReport(report?: PerformanceReport): string {
|
|
190
|
+
const r = report || this.generateReport();
|
|
191
|
+
const lines = [
|
|
192
|
+
'Performance Report',
|
|
193
|
+
'==================',
|
|
194
|
+
`Total Operations: ${r.totalOperations}`,
|
|
195
|
+
`Completed: ${r.completedOperations} | Failed: ${r.failedOperations}`,
|
|
196
|
+
`Average Duration: ${r.averageDuration.toFixed(2)}ms`,
|
|
197
|
+
`Total Duration: ${r.totalDuration.toFixed(2)}ms`
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
if (r.apiQuota) {
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push('API Quota:');
|
|
203
|
+
lines.push(
|
|
204
|
+
` Used: ${r.apiQuota.used}/${r.apiQuota.limit} (${r.apiQuota.percentUsed.toFixed(1)}%)`
|
|
205
|
+
);
|
|
206
|
+
lines.push(` Remaining: ${r.apiQuota.remaining}`);
|
|
207
|
+
lines.push(` Resets at: ${r.apiQuota.resetAt.toISOString()}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Global performance tracker instance
|
|
216
|
+
*/
|
|
217
|
+
let globalTracker: PerformanceTracker | undefined;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get or create global performance tracker
|
|
221
|
+
*/
|
|
222
|
+
export function getPerformanceTracker(): PerformanceTracker {
|
|
223
|
+
if (!globalTracker) {
|
|
224
|
+
globalTracker = new PerformanceTracker();
|
|
225
|
+
}
|
|
226
|
+
return globalTracker;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Track an async operation
|
|
231
|
+
*/
|
|
232
|
+
export async function trackOperation<T>(
|
|
233
|
+
name: string,
|
|
234
|
+
fn: () => Promise<T>,
|
|
235
|
+
metadata?: Record<string, unknown>
|
|
236
|
+
): Promise<T> {
|
|
237
|
+
const tracker = getPerformanceTracker();
|
|
238
|
+
const operationId = tracker.startOperation(name, metadata);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const result = await fn();
|
|
242
|
+
tracker.endOperation(operationId, 'completed');
|
|
243
|
+
return result;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
tracker.endOperation(operationId, 'failed');
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|