atlasia-ghost 1.0.1 → 1.1.0
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/AGENTS.md +8 -1
- package/README.md +33 -0
- package/core/analytics/api-server.js +253 -0
- package/core/analytics/examples/populate-sample-data.js +103 -0
- package/core/analytics/index.js +54 -1
- package/core/analytics/start-api-server.js +40 -0
- package/core/analytics/start-telemetry-server.js +50 -0
- package/core/analytics/telemetry-ws-server.js +319 -0
- package/core/dependency-resolver.js +462 -0
- package/core/dev-mode.js +633 -23
- package/core/examples/hot-reload-example.js +116 -0
- package/core/examples/v0-extension-migration-manifest.json +39 -0
- package/core/examples/v0-extension-migration-sample.js +149 -0
- package/core/extension-dependency-resolver.js +645 -0
- package/core/extension-deps-commands.js +134 -0
- package/core/extension-loader.js +290 -17
- package/core/extension-migrator.js +1506 -0
- package/core/hot-reload-websocket.js +478 -0
- package/core/hot-reload.js +395 -0
- package/core/index.js +4 -0
- package/core/manifest-schema.json +48 -8
- package/core/marketplace-backend/README.md +413 -0
- package/core/marketplace-backend/admin-dashboard.js +55 -0
- package/core/marketplace-backend/auth-manager.js +212 -0
- package/core/marketplace-backend/cli.js +57 -0
- package/core/marketplace-backend/client-example.js +219 -0
- package/core/marketplace-backend/database.js +506 -0
- package/core/marketplace-backend/download-tracker.js +65 -0
- package/core/marketplace-backend/index.js +19 -0
- package/core/marketplace-backend/manifest-validator.js +223 -0
- package/core/marketplace-backend/package.json +29 -0
- package/core/marketplace-backend/rate-limiter.js +88 -0
- package/core/marketplace-backend/security-scanner.js +280 -0
- package/core/marketplace-backend/server.js +428 -0
- package/core/marketplace.js +25 -3
- package/core/pipeline/audit.js +19 -5
- package/core/pipeline/execute.js +4 -2
- package/core/pipeline/index.js +6 -2
- package/core/pipeline/intercept.js +1 -1
- package/core/registry-client.js +254 -0
- package/core/runtime.js +120 -0
- package/core/template-wizard.js +335 -151
- package/core/templates/api-integration.js +343 -0
- package/core/templates/base-template.js +45 -0
- package/core/templates/file-processor.js +474 -0
- package/core/templates/gallery.js +50 -0
- package/core/templates/git-workflow.js +700 -0
- package/core/templates/testing.js +962 -0
- package/core/validators/entropy-validator.js +2 -2
- package/docs/DEPENDENCY_RESOLUTION.md +272 -0
- package/docs/DEVELOPER_TOOLKIT.md +72 -0
- package/docs/EXTENSION_DEPENDENCIES.md +435 -0
- package/docs/EXTENSION_MIGRATION.md +836 -0
- package/docs/HOT_RELOAD.md +541 -0
- package/docs/HOT_RELOAD_QUICK_START.md +302 -0
- package/docs/QUICK_REFERENCE.md +4 -0
- package/docs/REGISTRY_API.md +501 -0
- package/docs/TEMPLATE_GALLERY.md +629 -0
- package/docs/templates-quick-ref.md +38 -0
- package/extensions/ghost-git-extension/extension.js +44 -2
- package/extensions/ghost-git-extension/index.js +25 -2
- package/extensions/ghost-git-extension/manifest.json +5 -5
- package/ghost.js +342 -33
- package/package.json +5 -1
package/AGENTS.md
CHANGED
|
@@ -40,9 +40,15 @@
|
|
|
40
40
|
New commands and SDK for building extensions:
|
|
41
41
|
|
|
42
42
|
**CLI Commands:**
|
|
43
|
-
- `ghost extension init
|
|
43
|
+
- `ghost extension init` - Interactive template wizard with gallery
|
|
44
44
|
- `ghost extension validate [path]` - Validate manifest and permissions
|
|
45
45
|
|
|
46
|
+
**Template Gallery:** `core/templates/` - Production-ready templates:
|
|
47
|
+
- **API Integration** - REST/GraphQL client with auth (Bearer, API Key, OAuth)
|
|
48
|
+
- **File Processor** - Batch operations with progress tracking and transformations
|
|
49
|
+
- **Git Workflow** - Commit hooks with validation (pre-commit, commit-msg, conventional commits)
|
|
50
|
+
- **Testing** - Vitest/Jest setup with mocking, coverage, and E2E support
|
|
51
|
+
|
|
46
52
|
**SDK Package:** `packages/extension-sdk/` - @ghost/extension-sdk NPM package with:
|
|
47
53
|
- `ExtensionSDK` class - High-level API (requestFileRead, requestNetworkCall, requestGitExec)
|
|
48
54
|
- `IntentBuilder` - Build JSON-RPC intents
|
|
@@ -54,3 +60,4 @@ New commands and SDK for building extensions:
|
|
|
54
60
|
- `extension-examples.md` - Working examples (file processor, API integration, git helper)
|
|
55
61
|
- `DEVELOPER_TOOLKIT.md` - Complete toolkit guide
|
|
56
62
|
- `QUICK_REFERENCE.md` - Quick reference card
|
|
63
|
+
- `TEMPLATE_GALLERY.md` - Template gallery guide
|
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ Ghost includes a complete toolkit for building custom extensions:
|
|
|
91
91
|
|
|
92
92
|
- **`ghost extension init <name>`** - Scaffold a new extension project with boilerplate
|
|
93
93
|
- **`ghost extension validate [path]`** - Validate manifest syntax and simulate permissions
|
|
94
|
+
- **`ghost extension migrate [path]`** - Migrate v0.x extensions to v1.0.0 SDK
|
|
94
95
|
- **`ghost extension install <path>`** - Install extension locally
|
|
95
96
|
- **`ghost extension list`** - List installed extensions
|
|
96
97
|
- **`ghost extension info <id>`** - Show extension details
|
|
@@ -133,6 +134,7 @@ module.exports = MyExtension;
|
|
|
133
134
|
### Documentation
|
|
134
135
|
|
|
135
136
|
- 🛠️ [Developer Toolkit Guide](./docs/DEVELOPER_TOOLKIT.md) - Complete guide to extension development
|
|
137
|
+
- 🔄 [Extension Migration Guide](./docs/EXTENSION_MIGRATION.md) - Migrate v0.x to v1.0.0
|
|
136
138
|
- 📖 [Extension API Reference](./docs/extension-api.md) - I/O intent schema and examples
|
|
137
139
|
- 💡 [Extension Examples](./docs/extension-examples.md) - Working examples for common patterns
|
|
138
140
|
- 📦 [Extension SDK Package](./packages/extension-sdk/README.md) - SDK documentation
|
|
@@ -297,6 +299,37 @@ ghost version bump --from-commits --tag --output json
|
|
|
297
299
|
|
|
298
300
|
### Creating Extensions
|
|
299
301
|
|
|
302
|
+
#### 🎨 Quick Start with Template Gallery
|
|
303
|
+
|
|
304
|
+
Ghost CLI includes a comprehensive template gallery with pre-built patterns:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
# Interactive template selector
|
|
308
|
+
ghost extension init
|
|
309
|
+
|
|
310
|
+
# Or use specific template
|
|
311
|
+
ghost extension init my-api --template api-integration
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Available Templates:**
|
|
315
|
+
- `api-integration` - REST/GraphQL client with auth, retry, caching
|
|
316
|
+
- `file-processor` - Batch file operations with streaming
|
|
317
|
+
- `git-workflow` - Git hooks and conventional commits
|
|
318
|
+
- `testing` - Test infrastructure with mock RPC client
|
|
319
|
+
- `basic` - Simple minimal structure
|
|
320
|
+
- `typescript` - Type-safe development
|
|
321
|
+
- `advanced` - Production-ready with tests
|
|
322
|
+
|
|
323
|
+
Each template includes:
|
|
324
|
+
- ✅ Fully-implemented, commented code
|
|
325
|
+
- ✅ Complete test suite
|
|
326
|
+
- ✅ Comprehensive README with examples
|
|
327
|
+
- ✅ Best practices built-in
|
|
328
|
+
|
|
329
|
+
[📚 View Template Gallery →](./templates/README.md)
|
|
330
|
+
|
|
331
|
+
#### Manual Extension Creation
|
|
332
|
+
|
|
300
333
|
1. Create extension directory structure:
|
|
301
334
|
```
|
|
302
335
|
my-extension/
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const { AnalyticsPlatform } = require('./index');
|
|
3
|
+
|
|
4
|
+
class AnalyticsAPIServer {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.options = {
|
|
7
|
+
port: options.port || 9876,
|
|
8
|
+
host: options.host || 'localhost',
|
|
9
|
+
...options
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
this.analytics = new AnalyticsPlatform(options.analytics || {});
|
|
13
|
+
this.server = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async start() {
|
|
17
|
+
await this.analytics.initialize();
|
|
18
|
+
|
|
19
|
+
this.server = http.createServer((req, res) => {
|
|
20
|
+
this._handleRequest(req, res);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
this.server.listen(this.options.port, this.options.host, (err) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
reject(err);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(`[AnalyticsAPI] Server listening on http://${this.options.host}:${this.options.port}`);
|
|
29
|
+
resolve();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async stop() {
|
|
36
|
+
await this.analytics.shutdown();
|
|
37
|
+
|
|
38
|
+
if (this.server) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
this.server.close(() => {
|
|
41
|
+
console.log('[AnalyticsAPI] Server stopped');
|
|
42
|
+
resolve();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async _handleRequest(req, res) {
|
|
49
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
50
|
+
|
|
51
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
52
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
53
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
54
|
+
res.setHeader('Content-Type', 'application/json');
|
|
55
|
+
|
|
56
|
+
if (req.method === 'OPTIONS') {
|
|
57
|
+
res.writeHead(200);
|
|
58
|
+
res.end();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (url.pathname === '/api/analytics/dashboard') {
|
|
64
|
+
await this._handleDashboard(req, res, url);
|
|
65
|
+
} else if (url.pathname.startsWith('/api/analytics/performance/')) {
|
|
66
|
+
await this._handlePerformanceHistory(req, res, url);
|
|
67
|
+
} else if (url.pathname === '/api/analytics/extensions') {
|
|
68
|
+
await this._handleExtensionsList(req, res);
|
|
69
|
+
} else if (url.pathname.startsWith('/api/analytics/extension/')) {
|
|
70
|
+
await this._handleExtensionDetail(req, res, url);
|
|
71
|
+
} else {
|
|
72
|
+
res.writeHead(404);
|
|
73
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('[AnalyticsAPI] Error handling request:', error);
|
|
77
|
+
res.writeHead(500);
|
|
78
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async _handleDashboard(req, res, url) {
|
|
83
|
+
const timeRange = url.searchParams.get('timeRange') || '6h';
|
|
84
|
+
|
|
85
|
+
const allMetrics = this.analytics.collector.getAllMetrics();
|
|
86
|
+
const costs = [];
|
|
87
|
+
const alerts = this.analytics.performance.getAlerts();
|
|
88
|
+
|
|
89
|
+
for (const [extensionId, metrics] of Object.entries(allMetrics)) {
|
|
90
|
+
const costData = this.analytics.cost.getCostsByExtension(extensionId);
|
|
91
|
+
if (costData) {
|
|
92
|
+
costs.push({
|
|
93
|
+
extensionId,
|
|
94
|
+
totalCost: costData.total || 0,
|
|
95
|
+
resources: costData.breakdown || {
|
|
96
|
+
cpu: 0,
|
|
97
|
+
memory: 0,
|
|
98
|
+
io: 0,
|
|
99
|
+
network: 0,
|
|
100
|
+
storage: 0
|
|
101
|
+
},
|
|
102
|
+
billingPeriod: costData.period || 'current'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const crossExtensionCalls = this.analytics.tracing.getCrossExtensionCalls();
|
|
108
|
+
const extensionInteractions = this.analytics.tracing.getExtensionInteractions();
|
|
109
|
+
|
|
110
|
+
const callGraph = {
|
|
111
|
+
nodes: [],
|
|
112
|
+
edges: []
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (crossExtensionCalls && crossExtensionCalls.length > 0) {
|
|
116
|
+
const nodeMap = new Map();
|
|
117
|
+
|
|
118
|
+
crossExtensionCalls.forEach(call => {
|
|
119
|
+
const fromKey = `${call.from.extensionId}:${call.from.operation}`;
|
|
120
|
+
const toKey = `${call.to.extensionId}:${call.to.operation}`;
|
|
121
|
+
|
|
122
|
+
if (!nodeMap.has(fromKey)) {
|
|
123
|
+
nodeMap.set(fromKey, {
|
|
124
|
+
extensionId: call.from.extensionId,
|
|
125
|
+
operation: call.from.operation,
|
|
126
|
+
callCount: 0,
|
|
127
|
+
totalDuration: 0,
|
|
128
|
+
avgDuration: 0
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!nodeMap.has(toKey)) {
|
|
133
|
+
nodeMap.set(toKey, {
|
|
134
|
+
extensionId: call.to.extensionId,
|
|
135
|
+
operation: call.to.operation,
|
|
136
|
+
callCount: 0,
|
|
137
|
+
totalDuration: 0,
|
|
138
|
+
avgDuration: 0
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fromNode = nodeMap.get(fromKey);
|
|
143
|
+
const toNode = nodeMap.get(toKey);
|
|
144
|
+
|
|
145
|
+
fromNode.callCount += call.count;
|
|
146
|
+
toNode.callCount += call.count;
|
|
147
|
+
fromNode.totalDuration += call.totalDuration || 0;
|
|
148
|
+
toNode.totalDuration += call.totalDuration || 0;
|
|
149
|
+
|
|
150
|
+
callGraph.edges.push({
|
|
151
|
+
from: fromKey,
|
|
152
|
+
to: toKey,
|
|
153
|
+
callCount: call.count
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
nodeMap.forEach((node, key) => {
|
|
158
|
+
node.avgDuration = node.callCount > 0 ? node.totalDuration / node.callCount : 0;
|
|
159
|
+
callGraph.nodes.push(node);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const formattedAlerts = alerts.map(alert => ({
|
|
164
|
+
id: alert.id || `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
165
|
+
extensionId: alert.extensionId,
|
|
166
|
+
version: alert.version || '1.0.0',
|
|
167
|
+
severity: alert.severity || 'medium',
|
|
168
|
+
metric: alert.metric,
|
|
169
|
+
baselineValue: alert.baselineValue,
|
|
170
|
+
currentValue: alert.currentValue,
|
|
171
|
+
percentChange: alert.percentChange,
|
|
172
|
+
threshold: alert.threshold,
|
|
173
|
+
timestamp: alert.timestamp || Date.now()
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
const dashboardData = {
|
|
177
|
+
metrics: allMetrics,
|
|
178
|
+
costs,
|
|
179
|
+
alerts: formattedAlerts,
|
|
180
|
+
callGraph,
|
|
181
|
+
timestamp: Date.now()
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
res.writeHead(200);
|
|
185
|
+
res.end(JSON.stringify(dashboardData));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async _handlePerformanceHistory(req, res, url) {
|
|
189
|
+
const extensionId = url.pathname.split('/').pop();
|
|
190
|
+
const timeRange = url.searchParams.get('timeRange') || '6h';
|
|
191
|
+
|
|
192
|
+
const history = [];
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
const timeRangeMs = this._parseTimeRange(timeRange);
|
|
195
|
+
|
|
196
|
+
const invocations = Array.from(this.analytics.collector.metrics.values())
|
|
197
|
+
.filter(m => m.extensionId === extensionId && m.timestamp > (now - timeRangeMs))
|
|
198
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
199
|
+
|
|
200
|
+
const bucketSize = Math.max(1, Math.floor(invocations.length / 50));
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < invocations.length; i += bucketSize) {
|
|
203
|
+
const bucket = invocations.slice(i, i + bucketSize);
|
|
204
|
+
const durations = bucket
|
|
205
|
+
.filter(inv => inv.duration !== undefined)
|
|
206
|
+
.map(inv => inv.duration)
|
|
207
|
+
.sort((a, b) => a - b);
|
|
208
|
+
|
|
209
|
+
if (durations.length > 0) {
|
|
210
|
+
history.push({
|
|
211
|
+
timestamp: bucket[0].timestamp,
|
|
212
|
+
p50: durations[Math.floor(durations.length * 0.5)] || 0,
|
|
213
|
+
p95: durations[Math.floor(durations.length * 0.95)] || 0,
|
|
214
|
+
p99: durations[Math.floor(durations.length * 0.99)] || 0
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
res.writeHead(200);
|
|
220
|
+
res.end(JSON.stringify({ history }));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async _handleExtensionsList(req, res) {
|
|
224
|
+
const allMetrics = this.analytics.collector.getAllMetrics();
|
|
225
|
+
const extensions = Object.keys(allMetrics).map(extensionId => ({
|
|
226
|
+
id: extensionId,
|
|
227
|
+
metrics: allMetrics[extensionId]
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
res.writeHead(200);
|
|
231
|
+
res.end(JSON.stringify({ extensions }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async _handleExtensionDetail(req, res, url) {
|
|
235
|
+
const extensionId = url.pathname.split('/').pop();
|
|
236
|
+
const metrics = this.analytics.getExtensionMetrics(extensionId);
|
|
237
|
+
|
|
238
|
+
res.writeHead(200);
|
|
239
|
+
res.end(JSON.stringify(metrics));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_parseTimeRange(timeRange) {
|
|
243
|
+
const ranges = {
|
|
244
|
+
'1h': 60 * 60 * 1000,
|
|
245
|
+
'6h': 6 * 60 * 60 * 1000,
|
|
246
|
+
'24h': 24 * 60 * 60 * 1000,
|
|
247
|
+
'7d': 7 * 24 * 60 * 60 * 1000
|
|
248
|
+
};
|
|
249
|
+
return ranges[timeRange] || ranges['6h'];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = AnalyticsAPIServer;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { AnalyticsPlatform } = require('../index');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
async function populateSampleData() {
|
|
6
|
+
const analytics = new AnalyticsPlatform({
|
|
7
|
+
persistenceDir: path.join(os.homedir(), '.ghost', 'analytics'),
|
|
8
|
+
flushInterval: 60000,
|
|
9
|
+
retentionDays: 30
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
await analytics.initialize();
|
|
13
|
+
|
|
14
|
+
console.log('[Sample Data] Generating analytics data...');
|
|
15
|
+
|
|
16
|
+
const extensions = [
|
|
17
|
+
'ghost-git-extension',
|
|
18
|
+
'ghost-npm-extension',
|
|
19
|
+
'ghost-docker-extension',
|
|
20
|
+
'ghost-lint-extension'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const operations = {
|
|
24
|
+
'ghost-git-extension': ['status', 'commit', 'push', 'pull', 'branch'],
|
|
25
|
+
'ghost-npm-extension': ['install', 'test', 'build', 'publish'],
|
|
26
|
+
'ghost-docker-extension': ['build', 'run', 'ps', 'logs'],
|
|
27
|
+
'ghost-lint-extension': ['check', 'fix', 'format']
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < 200; i++) {
|
|
31
|
+
const extensionId = extensions[Math.floor(Math.random() * extensions.length)];
|
|
32
|
+
const method = operations[extensionId][Math.floor(Math.random() * operations[extensionId].length)];
|
|
33
|
+
|
|
34
|
+
const context = analytics.trackExtensionInvocation(extensionId, method, {
|
|
35
|
+
param1: 'value1',
|
|
36
|
+
param2: 'value2'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 50 + 10));
|
|
40
|
+
|
|
41
|
+
const duration = Math.random() * 200 + 10;
|
|
42
|
+
const success = Math.random() > 0.15;
|
|
43
|
+
|
|
44
|
+
analytics.trackResourceUsage(context, {
|
|
45
|
+
cpu: Math.random() * 5,
|
|
46
|
+
memory: Math.random() * 100 + 20,
|
|
47
|
+
io: Math.random() * 1024 * 10,
|
|
48
|
+
network: Math.random() * 1024 * 5
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (success) {
|
|
52
|
+
analytics.trackExtensionSuccess(context, { status: 'ok', data: {} });
|
|
53
|
+
} else {
|
|
54
|
+
analytics.trackExtensionFailure(context, new Error('Sample error'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
analytics.trackVersionMetric(extensionId, '1.0.0', {
|
|
58
|
+
duration,
|
|
59
|
+
cpu: Math.random() * 5,
|
|
60
|
+
memory: Math.random() * 100,
|
|
61
|
+
errorRate: success ? 0 : 1
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (Math.random() > 0.7) {
|
|
65
|
+
const targetExtension = extensions[Math.floor(Math.random() * extensions.length)];
|
|
66
|
+
if (targetExtension !== extensionId) {
|
|
67
|
+
const childSpanId = analytics.trackCrossExtensionCall(
|
|
68
|
+
context.spanId,
|
|
69
|
+
targetExtension,
|
|
70
|
+
'delegate',
|
|
71
|
+
{}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 30));
|
|
75
|
+
|
|
76
|
+
analytics.tracing.endSpan(childSpanId, 'success', {});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (i % 10 === 0) {
|
|
81
|
+
console.log(`[Sample Data] Generated ${i} invocations...`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const extensionId of extensions) {
|
|
86
|
+
analytics.performance.setBaseline(extensionId, '1.0.0');
|
|
87
|
+
|
|
88
|
+
analytics.trackVersionMetric(extensionId, '1.1.0', {
|
|
89
|
+
duration: Math.random() * 300 + 50,
|
|
90
|
+
cpu: Math.random() * 8,
|
|
91
|
+
memory: Math.random() * 150,
|
|
92
|
+
errorRate: 0.2
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await analytics.persist();
|
|
97
|
+
console.log('[Sample Data] Sample data generated successfully!');
|
|
98
|
+
console.log('[Sample Data] Start the API server with: node core/analytics/start-api-server.js');
|
|
99
|
+
|
|
100
|
+
await analytics.shutdown();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
populateSampleData().catch(console.error);
|
package/core/analytics/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const CostAttribution = require('./cost-attribution');
|
|
|
4
4
|
const PerformanceRegression = require('./performance-regression');
|
|
5
5
|
const DistributedTracing = require('./distributed-tracing');
|
|
6
6
|
const RecommendationEngine = require('./recommendation-engine');
|
|
7
|
+
const TelemetryWebSocketServer = require('./telemetry-ws-server');
|
|
7
8
|
const { EventEmitter } = require('events');
|
|
8
9
|
|
|
9
10
|
class AnalyticsPlatform extends EventEmitter {
|
|
@@ -17,6 +18,14 @@ class AnalyticsPlatform extends EventEmitter {
|
|
|
17
18
|
this.performance = new PerformanceRegression(options);
|
|
18
19
|
this.tracing = new DistributedTracing(options);
|
|
19
20
|
this.recommendations = new RecommendationEngine(options);
|
|
21
|
+
|
|
22
|
+
this.wsServer = null;
|
|
23
|
+
if (options.enableWebSocket !== false) {
|
|
24
|
+
this.wsServer = new TelemetryWebSocketServer({
|
|
25
|
+
port: options.wsPort || 9877,
|
|
26
|
+
host: options.wsHost || 'localhost'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
this._setupEventForwarding();
|
|
22
31
|
}
|
|
@@ -28,6 +37,14 @@ class AnalyticsPlatform extends EventEmitter {
|
|
|
28
37
|
await this.tracing.load();
|
|
29
38
|
await this.recommendations.load();
|
|
30
39
|
|
|
40
|
+
if (this.wsServer) {
|
|
41
|
+
try {
|
|
42
|
+
await this.wsServer.start();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('[AnalyticsPlatform] Failed to start WebSocket server:', error.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
this.emit('initialized');
|
|
32
49
|
}
|
|
33
50
|
|
|
@@ -51,6 +68,17 @@ class AnalyticsPlatform extends EventEmitter {
|
|
|
51
68
|
|
|
52
69
|
this.collector.recordSuccess(trackingContext.invocationId, result, duration);
|
|
53
70
|
this.tracing.endSpan(trackingContext.spanId, 'success', { resultSize: JSON.stringify(result).length });
|
|
71
|
+
|
|
72
|
+
if (this.wsServer) {
|
|
73
|
+
this.wsServer.broadcastToExtension(trackingContext.extensionId || this._getExtensionIdFromInvocation(trackingContext.invocationId), {
|
|
74
|
+
type: 'invocation-completed',
|
|
75
|
+
extensionId: trackingContext.extensionId || this._getExtensionIdFromInvocation(trackingContext.invocationId),
|
|
76
|
+
invocationId: trackingContext.invocationId,
|
|
77
|
+
status: 'success',
|
|
78
|
+
duration,
|
|
79
|
+
timestamp: Date.now()
|
|
80
|
+
});
|
|
81
|
+
}
|
|
54
82
|
}
|
|
55
83
|
|
|
56
84
|
trackExtensionFailure(trackingContext, error) {
|
|
@@ -58,6 +86,18 @@ class AnalyticsPlatform extends EventEmitter {
|
|
|
58
86
|
|
|
59
87
|
this.collector.recordFailure(trackingContext.invocationId, error, duration);
|
|
60
88
|
this.tracing.endSpan(trackingContext.spanId, 'error', { error: error.message });
|
|
89
|
+
|
|
90
|
+
if (this.wsServer) {
|
|
91
|
+
this.wsServer.broadcastToExtension(trackingContext.extensionId || this._getExtensionIdFromInvocation(trackingContext.invocationId), {
|
|
92
|
+
type: 'invocation-completed',
|
|
93
|
+
extensionId: trackingContext.extensionId || this._getExtensionIdFromInvocation(trackingContext.invocationId),
|
|
94
|
+
invocationId: trackingContext.invocationId,
|
|
95
|
+
status: 'failure',
|
|
96
|
+
duration,
|
|
97
|
+
error: error.message,
|
|
98
|
+
timestamp: Date.now()
|
|
99
|
+
});
|
|
100
|
+
}
|
|
61
101
|
}
|
|
62
102
|
|
|
63
103
|
trackResourceUsage(trackingContext, resources) {
|
|
@@ -169,6 +209,15 @@ class AnalyticsPlatform extends EventEmitter {
|
|
|
169
209
|
async shutdown() {
|
|
170
210
|
await this.persist();
|
|
171
211
|
this.collector.shutdown();
|
|
212
|
+
|
|
213
|
+
if (this.wsServer) {
|
|
214
|
+
try {
|
|
215
|
+
await this.wsServer.stop();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('[AnalyticsPlatform] Failed to stop WebSocket server:', error.message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
172
221
|
this.emit('shutdown');
|
|
173
222
|
}
|
|
174
223
|
|
|
@@ -204,6 +253,8 @@ class AnalyticsPlatform extends EventEmitter {
|
|
|
204
253
|
}
|
|
205
254
|
}
|
|
206
255
|
|
|
256
|
+
const AnalyticsAPIServer = require('./api-server');
|
|
257
|
+
|
|
207
258
|
module.exports = {
|
|
208
259
|
AnalyticsPlatform,
|
|
209
260
|
AnalyticsCollector,
|
|
@@ -211,5 +262,7 @@ module.exports = {
|
|
|
211
262
|
CostAttribution,
|
|
212
263
|
PerformanceRegression,
|
|
213
264
|
DistributedTracing,
|
|
214
|
-
RecommendationEngine
|
|
265
|
+
RecommendationEngine,
|
|
266
|
+
AnalyticsAPIServer,
|
|
267
|
+
TelemetryWebSocketServer
|
|
215
268
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { AnalyticsAPIServer } = require('./index');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const server = new AnalyticsAPIServer({
|
|
8
|
+
port: 9876,
|
|
9
|
+
host: 'localhost',
|
|
10
|
+
analytics: {
|
|
11
|
+
persistenceDir: path.join(os.homedir(), '.ghost', 'analytics'),
|
|
12
|
+
flushInterval: 60000,
|
|
13
|
+
retentionDays: 30
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
try {
|
|
19
|
+
await server.start();
|
|
20
|
+
console.log('[Analytics] API Server started successfully');
|
|
21
|
+
console.log('[Analytics] Access dashboard at: http://localhost:9876/api/analytics/dashboard');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('[Analytics] Failed to start API server:', error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process.on('SIGINT', async () => {
|
|
29
|
+
console.log('\n[Analytics] Shutting down...');
|
|
30
|
+
await server.stop();
|
|
31
|
+
process.exit(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
process.on('SIGTERM', async () => {
|
|
35
|
+
console.log('\n[Analytics] Shutting down...');
|
|
36
|
+
await server.stop();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
main();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { AnalyticsPlatform } = require('./index');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const analytics = new AnalyticsPlatform({
|
|
8
|
+
persistenceDir: path.join(os.homedir(), '.ghost', 'analytics'),
|
|
9
|
+
enableWebSocket: true,
|
|
10
|
+
wsPort: 9877,
|
|
11
|
+
wsHost: 'localhost'
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
async function startServer() {
|
|
15
|
+
try {
|
|
16
|
+
await analytics.initialize();
|
|
17
|
+
console.log('[TelemetryServer] Analytics Platform initialized');
|
|
18
|
+
console.log('[TelemetryServer] WebSocket server running on ws://localhost:9877/telemetry');
|
|
19
|
+
console.log('[TelemetryServer] Press Ctrl+C to stop');
|
|
20
|
+
|
|
21
|
+
analytics.on('invocation-started', (event) => {
|
|
22
|
+
console.log(`[Invocation] Started: ${event.extensionId} - ${event.method}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
analytics.on('invocation-completed', (event) => {
|
|
26
|
+
console.log(`[Invocation] Completed: ${event.extensionId} - ${event.status} (${event.duration}ms)`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
analytics.on('regression-detected', (alert) => {
|
|
30
|
+
console.log(`[Alert] Regression detected: ${alert.extensionId} - ${alert.severity}`);
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('[TelemetryServer] Failed to start:', error.message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
process.on('SIGINT', async () => {
|
|
39
|
+
console.log('\n[TelemetryServer] Shutting down...');
|
|
40
|
+
await analytics.shutdown();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
process.on('SIGTERM', async () => {
|
|
45
|
+
console.log('\n[TelemetryServer] Shutting down...');
|
|
46
|
+
await analytics.shutdown();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
startServer();
|