fraim 2.0.361 → 2.0.362-next.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.
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnalyticsReadQueue = void 0;
4
+ exports.analyticsRetryAfterMs = analyticsRetryAfterMs;
5
+ exports.analyticsReadErrorResponse = analyticsReadErrorResponse;
6
+ /** Admission and retry belong to analytics reads, never telemetry writes. */
7
+ function analyticsRetryAfterMs(error) {
8
+ const message = String(error?.message || '');
9
+ if (error?.code !== 16500 && error?.code !== 429 && error?.status !== 429 &&
10
+ error?.statusCode !== 429 && !/TooManyRequests|\b429\b/.test(message))
11
+ return undefined;
12
+ const delay = Number(error?.RetryAfterMs ?? error?.retryAfterMs ??
13
+ message.match(/RetryAfterMs\s*[=:]\s*(\d+)/i)?.[1] ?? 100);
14
+ return Number.isFinite(delay) && delay >= 0 ? delay : 100;
15
+ }
16
+ /** Keep retry advice and public error messages consistent across analytics routes. */
17
+ function analyticsReadErrorResponse(error) {
18
+ const status = [400, 401, 403, 404, 503].includes(error?.status) ? error.status : 500;
19
+ const headers = {};
20
+ if (status === 503 && error.retryAfterSeconds)
21
+ headers['Retry-After'] = String(error.retryAfterSeconds);
22
+ return { status, headers, body: { error: status === 500 ? 'Internal server error' : error.message } };
23
+ }
24
+ class AnalyticsReadQueue {
25
+ constructor(concurrency = 4) {
26
+ this.concurrency = concurrency;
27
+ this.active = 0;
28
+ this.waiting = [];
29
+ if (!Number.isInteger(concurrency) || concurrency < 1)
30
+ throw new Error('Invalid read concurrency');
31
+ }
32
+ async run(read) {
33
+ if (this.active >= this.concurrency)
34
+ await new Promise(resolve => this.waiting.push(resolve));
35
+ else
36
+ this.active++;
37
+ try {
38
+ for (let attempt = 0;; attempt++) {
39
+ try {
40
+ return await read();
41
+ }
42
+ catch (error) {
43
+ const delay = analyticsRetryAfterMs(error);
44
+ if (delay === undefined)
45
+ throw error;
46
+ console.info(`[analytics] throttle=429 attempt=${attempt + 1} retry_after_ms=${delay}`);
47
+ // Do not retry early when Cosmos asks for a longer pause than
48
+ // this request's bounded retry budget. Return that pause to the caller.
49
+ if (attempt >= 2 || delay > 2000) {
50
+ throw Object.assign(new Error('Analytics is busy. Please retry shortly.'), {
51
+ status: 503, retryAfterSeconds: Math.max(1, Math.ceil(delay / 1000)), cause: error,
52
+ });
53
+ }
54
+ await new Promise(resolve => setTimeout(resolve, delay));
55
+ }
56
+ }
57
+ }
58
+ finally {
59
+ const next = this.waiting.shift();
60
+ if (next)
61
+ next();
62
+ else
63
+ this.active--;
64
+ }
65
+ }
66
+ }
67
+ exports.AnalyticsReadQueue = AnalyticsReadQueue;
@@ -206,6 +206,27 @@ class UsageAnalyticsService {
206
206
  }
207
207
  return dominant;
208
208
  }
209
+ async getTeamMemberSummaries(period, userIds, repoIdentifier, startDate, endDate) {
210
+ const data = await this.dbService.getTeamMemberSummaryData(userIds, this.resolveTimeWindow(period, startDate, endDate), repoIdentifier);
211
+ // Resolve each distinct job once; historical categories follow the current catalog.
212
+ const categories = new Map();
213
+ for (const row of data) {
214
+ for (const component of row.components) {
215
+ const key = JSON.stringify([component.name, component.category]);
216
+ if (!categories.has(key))
217
+ categories.set(key, await this.resolveCanonicalCategory(component.name, 'job', component.category) || 'uncategorized');
218
+ }
219
+ }
220
+ return data.map(({ components, ...summary }) => {
221
+ const totals = new Map();
222
+ for (const component of components) {
223
+ const category = categories.get(JSON.stringify([component.name, component.category])) || 'uncategorized';
224
+ totals.set(category, (totals.get(category) ?? 0) + component.count);
225
+ }
226
+ const dominant = [...totals].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
227
+ return { ...summary, dominantCategory: dominant?.[0] ?? null, dominantCategoryCount: dominant?.[1] ?? 0 };
228
+ });
229
+ }
209
230
  /**
210
231
  * Export usage data as CSV
211
232
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.361",
3
+ "version": "2.0.362-next.2",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {