@littlebearapps/platform-admin-sdk 1.4.2 → 1.5.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/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/package.json.hbs +12 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +2 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Billing & Enterprise Collectors
|
|
3
|
+
*
|
|
4
|
+
* Collects billing data from GitHub APIs:
|
|
5
|
+
* - Organization billing usage (Actions, GHEC, GHAS, Packages, LFS, Copilot)
|
|
6
|
+
* - Enterprise consumed licenses (seat tracking) — optional
|
|
7
|
+
*
|
|
8
|
+
* Requires:
|
|
9
|
+
* - GITHUB_TOKEN: PAT with admin:org or read:org scope for org billing
|
|
10
|
+
* - GITHUB_ORG: Your GitHub organization slug
|
|
11
|
+
* - GITHUB_PAT: PAT with read:enterprise scope for enterprise licenses (optional)
|
|
12
|
+
* - GITHUB_ENTERPRISE_SLUG: Enterprise slug for license queries (optional)
|
|
13
|
+
*
|
|
14
|
+
* This is a FLOW metric (billing accumulates) - safe to SUM.
|
|
15
|
+
*
|
|
16
|
+
* @see https://docs.github.com/en/rest/billing
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
Env,
|
|
21
|
+
GitHubBillingData,
|
|
22
|
+
GitHubPlanInfo,
|
|
23
|
+
GitHubUsageItem,
|
|
24
|
+
GitHubPlanInclusions,
|
|
25
|
+
} from '../shared';
|
|
26
|
+
import { fetchWithRetry } from '../shared';
|
|
27
|
+
import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
|
|
28
|
+
import type { ExternalCollector } from './index';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Enterprise consumed licenses data.
|
|
32
|
+
*/
|
|
33
|
+
export interface GitHubEnterpriseSeats {
|
|
34
|
+
totalSeatsConsumed: number;
|
|
35
|
+
totalSeatsPurchased: number;
|
|
36
|
+
licenseBreakdown: {
|
|
37
|
+
visualStudio: number;
|
|
38
|
+
enterprise: number;
|
|
39
|
+
unlicensed: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Combined external metrics result for GitHub.
|
|
45
|
+
*/
|
|
46
|
+
export interface GitHubExternalMetrics {
|
|
47
|
+
billing: GitHubBillingData | null;
|
|
48
|
+
enterpriseSeats: GitHubEnterpriseSeats | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get GitHub plan inclusions based on plan name.
|
|
53
|
+
* @see https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-actions/about-billing-for-github-actions
|
|
54
|
+
*/
|
|
55
|
+
export function getGitHubPlanInclusions(planName: string): GitHubPlanInclusions {
|
|
56
|
+
const lowerPlan = planName.toLowerCase();
|
|
57
|
+
|
|
58
|
+
if (lowerPlan.includes('enterprise')) {
|
|
59
|
+
return {
|
|
60
|
+
actionsMinutesIncluded: 50000,
|
|
61
|
+
actionsStorageGbIncluded: 50,
|
|
62
|
+
packagesStorageGbIncluded: 50,
|
|
63
|
+
codespacesHoursIncluded: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (lowerPlan.includes('team')) {
|
|
68
|
+
return {
|
|
69
|
+
actionsMinutesIncluded: 3000,
|
|
70
|
+
actionsStorageGbIncluded: 2,
|
|
71
|
+
packagesStorageGbIncluded: 2,
|
|
72
|
+
codespacesHoursIncluded: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (lowerPlan.includes('pro')) {
|
|
77
|
+
return {
|
|
78
|
+
actionsMinutesIncluded: 3000,
|
|
79
|
+
actionsStorageGbIncluded: 2,
|
|
80
|
+
packagesStorageGbIncluded: 2,
|
|
81
|
+
codespacesHoursIncluded: 180,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// GitHub Free (default)
|
|
86
|
+
return {
|
|
87
|
+
actionsMinutesIncluded: 2000,
|
|
88
|
+
actionsStorageGbIncluded: 0.5,
|
|
89
|
+
packagesStorageGbIncluded: 0.5,
|
|
90
|
+
codespacesHoursIncluded: 120,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Collect GitHub billing data using the usage API and org plan info.
|
|
96
|
+
* Requires GITHUB_ORG to be set (your GitHub organization slug).
|
|
97
|
+
*/
|
|
98
|
+
export async function collectGitHubBilling(env: Env): Promise<GitHubBillingData | null> {
|
|
99
|
+
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:github-billing');
|
|
100
|
+
if (!env.GITHUB_TOKEN) {
|
|
101
|
+
log.info('No GITHUB_TOKEN configured, skipping billing collection');
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (!env.GITHUB_ORG) {
|
|
105
|
+
log.info('No GITHUB_ORG configured, skipping billing collection');
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const org = env.GITHUB_ORG;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const headers = {
|
|
113
|
+
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
|
|
114
|
+
Accept: 'application/vnd.github+json',
|
|
115
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
116
|
+
'User-Agent': 'platform-usage-worker/1.0',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Verify token scopes
|
|
120
|
+
const scopeCheck = await fetchWithRetry('https://api.github.com/user', { headers });
|
|
121
|
+
const scopes = scopeCheck.headers.get('X-OAuth-Scopes') || '';
|
|
122
|
+
if (scopes && !scopes.includes('admin:org') && !scopes.includes('read:org')) {
|
|
123
|
+
log.warn('Token may lack admin:org or read:org scope for billing access');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fetch org plan info
|
|
127
|
+
const orgResponse = await fetchWithRetry(`https://api.github.com/orgs/${org}`, { headers });
|
|
128
|
+
let planInfo: GitHubPlanInfo = {
|
|
129
|
+
planName: 'unknown',
|
|
130
|
+
filledSeats: 0,
|
|
131
|
+
totalSeats: 0,
|
|
132
|
+
privateRepos: 0,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (orgResponse.ok) {
|
|
136
|
+
const orgData = (await orgResponse.json()) as {
|
|
137
|
+
plan?: { name: string; filled_seats: number; seats: number; private_repos: number };
|
|
138
|
+
};
|
|
139
|
+
if (orgData.plan) {
|
|
140
|
+
planInfo = {
|
|
141
|
+
planName: orgData.plan.name,
|
|
142
|
+
filledSeats: orgData.plan.filled_seats || 0,
|
|
143
|
+
totalSeats: orgData.plan.seats || 0,
|
|
144
|
+
privateRepos: orgData.plan.private_repos || 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
log.warn('Could not fetch org info', { status: orgResponse.status });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fetch billing usage
|
|
152
|
+
const response = await fetchWithRetry(
|
|
153
|
+
`https://api.github.com/orgs/${org}/settings/billing/usage`,
|
|
154
|
+
{ headers }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const errorText = await response.text();
|
|
159
|
+
log.error(`Billing API returned ${response.status}: ${errorText}`);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const data = (await response.json()) as { usageItems: GitHubUsageItem[] };
|
|
164
|
+
|
|
165
|
+
if (!data.usageItems || !Array.isArray(data.usageItems)) {
|
|
166
|
+
log.error('Invalid response format - no usageItems array');
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Aggregate usage by product/SKU
|
|
171
|
+
const result: GitHubBillingData = {
|
|
172
|
+
plan: planInfo,
|
|
173
|
+
actionsMinutes: 0,
|
|
174
|
+
actionsMinutesCost: 0,
|
|
175
|
+
actionsStorageGbHours: 0,
|
|
176
|
+
actionsStorageCost: 0,
|
|
177
|
+
ghecUserMonths: 0,
|
|
178
|
+
ghecCost: 0,
|
|
179
|
+
ghasCodeSecurityUserMonths: 0,
|
|
180
|
+
ghasCodeSecurityCost: 0,
|
|
181
|
+
ghasSecretProtectionUserMonths: 0,
|
|
182
|
+
ghasSecretProtectionCost: 0,
|
|
183
|
+
packagesStorageGb: 0,
|
|
184
|
+
packagesStorageCost: 0,
|
|
185
|
+
packagesBandwidthGb: 0,
|
|
186
|
+
packagesBandwidthCost: 0,
|
|
187
|
+
lfsStorageGb: 0,
|
|
188
|
+
lfsStorageCost: 0,
|
|
189
|
+
lfsBandwidthGb: 0,
|
|
190
|
+
lfsBandwidthCost: 0,
|
|
191
|
+
copilotSeats: 0,
|
|
192
|
+
copilotCost: 0,
|
|
193
|
+
totalNetCost: 0,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
for (const item of data.usageItems) {
|
|
197
|
+
result.totalNetCost += item.netAmount;
|
|
198
|
+
|
|
199
|
+
if (item.product === 'actions') {
|
|
200
|
+
if (
|
|
201
|
+
item.sku === 'Actions Linux' ||
|
|
202
|
+
item.sku === 'Actions macOS' ||
|
|
203
|
+
item.sku === 'Actions Windows'
|
|
204
|
+
) {
|
|
205
|
+
result.actionsMinutes += item.quantity;
|
|
206
|
+
result.actionsMinutesCost += item.netAmount;
|
|
207
|
+
} else if (item.sku === 'Actions storage') {
|
|
208
|
+
result.actionsStorageGbHours += item.quantity;
|
|
209
|
+
result.actionsStorageCost += item.netAmount;
|
|
210
|
+
}
|
|
211
|
+
} else if (item.product === 'ghec') {
|
|
212
|
+
result.ghecUserMonths += item.quantity;
|
|
213
|
+
result.ghecCost += item.netAmount;
|
|
214
|
+
} else if (item.product === 'ghas') {
|
|
215
|
+
if (item.sku === 'Code Security') {
|
|
216
|
+
result.ghasCodeSecurityUserMonths += item.quantity;
|
|
217
|
+
result.ghasCodeSecurityCost += item.netAmount;
|
|
218
|
+
} else if (item.sku === 'Secret Protection') {
|
|
219
|
+
result.ghasSecretProtectionUserMonths += item.quantity;
|
|
220
|
+
result.ghasSecretProtectionCost += item.netAmount;
|
|
221
|
+
}
|
|
222
|
+
} else if (item.product === 'packages') {
|
|
223
|
+
if (item.unitType === 'gb' || item.sku?.toLowerCase().includes('storage')) {
|
|
224
|
+
result.packagesStorageGb += item.quantity;
|
|
225
|
+
result.packagesStorageCost += item.netAmount;
|
|
226
|
+
} else {
|
|
227
|
+
result.packagesBandwidthGb += item.quantity;
|
|
228
|
+
result.packagesBandwidthCost += item.netAmount;
|
|
229
|
+
}
|
|
230
|
+
} else if (item.product === 'git_lfs') {
|
|
231
|
+
if (item.sku?.toLowerCase().includes('storage')) {
|
|
232
|
+
result.lfsStorageGb += item.quantity;
|
|
233
|
+
result.lfsStorageCost += item.netAmount;
|
|
234
|
+
} else {
|
|
235
|
+
result.lfsBandwidthGb += item.quantity;
|
|
236
|
+
result.lfsBandwidthCost += item.netAmount;
|
|
237
|
+
}
|
|
238
|
+
} else if (item.product === 'copilot') {
|
|
239
|
+
result.copilotSeats += item.quantity;
|
|
240
|
+
result.copilotCost += item.netAmount;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
log.info('Collected billing', {
|
|
245
|
+
actionsMinutes: result.actionsMinutes,
|
|
246
|
+
ghecUserMonths: result.ghecUserMonths,
|
|
247
|
+
totalNetCost: result.totalNetCost,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
log.error('Failed to collect billing', error);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Collect GitHub Enterprise consumed licenses.
|
|
259
|
+
* Requires read:enterprise scope and GITHUB_ENTERPRISE_SLUG.
|
|
260
|
+
*/
|
|
261
|
+
export async function collectGitHubEnterpriseSeats(
|
|
262
|
+
env: Env
|
|
263
|
+
): Promise<GitHubEnterpriseSeats | null> {
|
|
264
|
+
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:github-enterprise');
|
|
265
|
+
|
|
266
|
+
const token = env.GITHUB_PAT || env.GITHUB_TOKEN;
|
|
267
|
+
const enterpriseSlug = env.GITHUB_ENTERPRISE_SLUG;
|
|
268
|
+
|
|
269
|
+
if (!token) {
|
|
270
|
+
log.info('No GitHub token configured, skipping enterprise seat collection');
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!enterpriseSlug) {
|
|
275
|
+
log.info('No GITHUB_ENTERPRISE_SLUG configured, skipping enterprise seat collection');
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const headers = {
|
|
281
|
+
Authorization: `Bearer ${token}`,
|
|
282
|
+
Accept: 'application/vnd.github+json',
|
|
283
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
284
|
+
'User-Agent': 'platform-usage-worker/1.0',
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const response = await fetchWithRetry(
|
|
288
|
+
`https://api.github.com/enterprises/${enterpriseSlug}/consumed-licenses`,
|
|
289
|
+
{ headers }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const errorText = await response.text();
|
|
294
|
+
if (response.status === 404) {
|
|
295
|
+
log.info('Enterprise not found or no access', { enterpriseSlug });
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (response.status === 403) {
|
|
299
|
+
log.warn('Token lacks read:enterprise scope for consumed-licenses API');
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
log.error(`Enterprise API returned ${response.status}: ${errorText}`);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const data = (await response.json()) as {
|
|
307
|
+
total_seats_consumed?: number;
|
|
308
|
+
total_seats_purchased?: number;
|
|
309
|
+
users?: Array<{
|
|
310
|
+
visual_studio_subscription_user?: boolean;
|
|
311
|
+
license_type?: string;
|
|
312
|
+
}>;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const result: GitHubEnterpriseSeats = {
|
|
316
|
+
totalSeatsConsumed: data.total_seats_consumed || 0,
|
|
317
|
+
totalSeatsPurchased: data.total_seats_purchased || 0,
|
|
318
|
+
licenseBreakdown: { visualStudio: 0, enterprise: 0, unlicensed: 0 },
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
if (data.users && Array.isArray(data.users)) {
|
|
322
|
+
for (const user of data.users) {
|
|
323
|
+
if (user.visual_studio_subscription_user) {
|
|
324
|
+
result.licenseBreakdown.visualStudio++;
|
|
325
|
+
} else if (user.license_type === 'Enterprise') {
|
|
326
|
+
result.licenseBreakdown.enterprise++;
|
|
327
|
+
} else {
|
|
328
|
+
result.licenseBreakdown.unlicensed++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
log.info('Collected enterprise seats', {
|
|
334
|
+
consumed: result.totalSeatsConsumed,
|
|
335
|
+
purchased: result.totalSeatsPurchased,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return result;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
log.error('Failed to collect enterprise seats', error);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Collect all GitHub external metrics (billing + enterprise seats).
|
|
347
|
+
*/
|
|
348
|
+
export async function collectGitHubMetrics(env: Env): Promise<GitHubExternalMetrics> {
|
|
349
|
+
const [billing, enterpriseSeats] = await Promise.all([
|
|
350
|
+
collectGitHubBilling(env),
|
|
351
|
+
collectGitHubEnterpriseSeats(env),
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
return { billing, enterpriseSeats };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Collector registration for use in collectors/index.ts COLLECTORS array */
|
|
358
|
+
export const githubCollector: ExternalCollector<GitHubExternalMetrics> = {
|
|
359
|
+
name: 'github',
|
|
360
|
+
collect: collectGitHubMetrics,
|
|
361
|
+
defaultValue: { billing: null, enterpriseSeats: null },
|
|
362
|
+
};
|
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
* Framework for collecting billing and usage data from external providers.
|
|
5
5
|
* Each collector handles errors gracefully - one failure doesn't stop others.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - GitHub: Org billing + Enterprise consumed licenses
|
|
10
|
-
* - OpenAI: Organization usage (Admin API)
|
|
11
|
-
* - Anthropic: Organization usage (Admin API)
|
|
12
|
-
* - Stripe: Revenue and subscription data
|
|
7
|
+
* 10 built-in collectors are available. Uncomment the imports for providers
|
|
8
|
+
* you use, then add them to the COLLECTORS array below.
|
|
13
9
|
*
|
|
14
10
|
* Metric types:
|
|
15
11
|
* - FLOW metrics: Usage that accumulates (requests, tokens) - safe to SUM
|
|
16
12
|
* - STOCK metrics: Point-in-time values (balance, quota) - do NOT SUM
|
|
13
|
+
*
|
|
14
|
+
* See example.ts for a collector template or custom-http.ts for a factory
|
|
15
|
+
* that creates collectors from config objects.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
import type { Env } from '../shared';
|
|
@@ -39,8 +38,6 @@ export interface ExternalCollector<T> {
|
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* Combined external metrics from all providers.
|
|
42
|
-
*
|
|
43
|
-
* TODO: Add your provider result types here.
|
|
44
41
|
*/
|
|
45
42
|
export interface ExternalMetrics {
|
|
46
43
|
/** Results keyed by collector name */
|
|
@@ -58,18 +55,37 @@ export interface ExternalMetrics {
|
|
|
58
55
|
/**
|
|
59
56
|
* Register your collectors here.
|
|
60
57
|
*
|
|
61
|
-
*
|
|
58
|
+
* Uncomment the imports for the providers you use, then add them to the
|
|
59
|
+
* COLLECTORS array. Each collector checks for its API key at runtime and
|
|
60
|
+
* returns null if not configured — safe to register even if unused.
|
|
62
61
|
*
|
|
63
|
-
*
|
|
62
|
+
* FLOW metrics (usage — safe to SUM):
|
|
64
63
|
* import { openaiCollector } from './openai';
|
|
64
|
+
* import { anthropicCollector } from './anthropic';
|
|
65
|
+
* import { githubCollector } from './github';
|
|
66
|
+
* import { stripeCollector } from './stripe';
|
|
67
|
+
* import { apifyCollector } from './apify';
|
|
68
|
+
* import { resendCollector } from './resend';
|
|
69
|
+
* import { geminiCollector } from './gemini';
|
|
70
|
+
*
|
|
71
|
+
* STOCK metrics (gauge — do NOT SUM):
|
|
72
|
+
* import { deepseekCollector } from './deepseek';
|
|
73
|
+
* import { minimaxCollector } from './minimax';
|
|
65
74
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* openaiCollector,
|
|
69
|
-
* ];
|
|
75
|
+
* Custom REST API (factory):
|
|
76
|
+
* import { createCustomCollector } from './custom-http';
|
|
70
77
|
*/
|
|
71
78
|
const COLLECTORS: ExternalCollector<unknown>[] = [
|
|
72
|
-
//
|
|
79
|
+
// Uncomment the collectors you need:
|
|
80
|
+
// openaiCollector,
|
|
81
|
+
// anthropicCollector,
|
|
82
|
+
// githubCollector,
|
|
83
|
+
// stripeCollector,
|
|
84
|
+
// apifyCollector,
|
|
85
|
+
// resendCollector,
|
|
86
|
+
// deepseekCollector,
|
|
87
|
+
// minimaxCollector,
|
|
88
|
+
// geminiCollector,
|
|
73
89
|
];
|
|
74
90
|
|
|
75
91
|
// =============================================================================
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimax Quota Collector
|
|
3
|
+
*
|
|
4
|
+
* Collects remaining quota from the Minimax API.
|
|
5
|
+
* This is a STOCK metric (quota remaining), not a FLOW metric (usage).
|
|
6
|
+
* Do NOT sum these values in dashboards - display as a gauge/runway indicator.
|
|
7
|
+
*
|
|
8
|
+
* Note: The exact API endpoint may need adjustment based on Minimax documentation.
|
|
9
|
+
* The coding_plan endpoint provides quota information for coding/API usage.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Env, MinimaxQuotaData } from '../shared';
|
|
13
|
+
import { fetchWithRetry } from '../shared';
|
|
14
|
+
import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
|
|
15
|
+
import type { ExternalCollector } from './index';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collect Minimax remaining quota.
|
|
19
|
+
* Returns the current quota snapshot - this is NOT daily usage.
|
|
20
|
+
*/
|
|
21
|
+
export async function collectMinimaxQuota(env: Env): Promise<MinimaxQuotaData | null> {
|
|
22
|
+
const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:minimax');
|
|
23
|
+
if (!env.MINIMAX_API_KEY) {
|
|
24
|
+
log.info('No MINIMAX_API_KEY configured, skipping quota collection');
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetchWithRetry('https://api.minimax.chat/v1/user/coding_plan', {
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${env.MINIMAX_API_KEY}`,
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const errorText = await response.text();
|
|
38
|
+
if (response.status === 401 || response.status === 403) {
|
|
39
|
+
log.info('Quota API access denied - check API key permissions', {
|
|
40
|
+
status: response.status,
|
|
41
|
+
});
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (response.status === 404) {
|
|
45
|
+
log.info('Minimax quota endpoint not found - may need API endpoint update', {
|
|
46
|
+
status: response.status,
|
|
47
|
+
});
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
log.error(`Quota API returned ${response.status}: ${errorText}`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const data = (await response.json()) as {
|
|
55
|
+
quota?: number;
|
|
56
|
+
total_quota?: number;
|
|
57
|
+
rest_quota?: number;
|
|
58
|
+
used_quota?: number;
|
|
59
|
+
plan_type?: string;
|
|
60
|
+
remaining_prompts?: number;
|
|
61
|
+
total_prompts?: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Handle different possible response formats
|
|
65
|
+
let remainingQuota = 0;
|
|
66
|
+
let totalQuota = 0;
|
|
67
|
+
|
|
68
|
+
if (typeof data.rest_quota === 'number') {
|
|
69
|
+
remainingQuota = data.rest_quota;
|
|
70
|
+
totalQuota = data.total_quota || data.rest_quota + (data.used_quota || 0);
|
|
71
|
+
} else if (typeof data.remaining_prompts === 'number') {
|
|
72
|
+
remainingQuota = data.remaining_prompts;
|
|
73
|
+
totalQuota = data.total_prompts || data.remaining_prompts;
|
|
74
|
+
} else if (typeof data.quota === 'number') {
|
|
75
|
+
remainingQuota = data.quota;
|
|
76
|
+
totalQuota = data.total_quota || data.quota;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const usagePercentage = totalQuota > 0 ? ((totalQuota - remainingQuota) / totalQuota) * 100 : 0;
|
|
80
|
+
|
|
81
|
+
const result: MinimaxQuotaData = {
|
|
82
|
+
remainingQuota,
|
|
83
|
+
totalQuota,
|
|
84
|
+
usagePercentage,
|
|
85
|
+
planType: data.plan_type || 'coding_plan',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
log.info('Collected Minimax quota', {
|
|
89
|
+
remainingQuota: result.remainingQuota,
|
|
90
|
+
totalQuota: result.totalQuota,
|
|
91
|
+
usagePercentage: result.usagePercentage.toFixed(1),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
log.error('Failed to collect Minimax quota', error);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Collector registration for use in collectors/index.ts COLLECTORS array */
|
|
102
|
+
export const minimaxCollector: ExternalCollector<MinimaxQuotaData | null> = {
|
|
103
|
+
name: 'minimax',
|
|
104
|
+
collect: collectMinimaxQuota,
|
|
105
|
+
defaultValue: null,
|
|
106
|
+
};
|