@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
394 lines (393 loc) • 15 kB
JavaScript
/**
* Advanced Analytics Service Layer
* Handles metrics collection, aggregation, cost analysis, and projections.
*/
import { calculateAdvancedCost } from "./pricing.js";
import { InMemoryAnalyticsStorage } from "./storage.js";
export class AnalyticsService {
storage;
constructor(storage) {
this.storage = storage || new InMemoryAnalyticsStorage();
}
/**
* Helper to parse dynamic time range formats safely
*/
parseTimeRange(timeRange) {
if (!timeRange) {
return null;
}
if (typeof timeRange === "object" &&
"start" in timeRange &&
"end" in timeRange) {
const startObj = timeRange.start;
const endObj = timeRange.end;
const start = startObj instanceof Date
? startObj.getTime()
: new Date(startObj).getTime();
const end = endObj instanceof Date
? endObj.getTime()
: new Date(endObj).getTime();
if (!Number.isNaN(start) && !Number.isNaN(end)) {
return { start, end };
}
}
if (typeof timeRange === "string") {
const now = Date.now();
const dayMs = 24 * 3600 * 1000;
if (timeRange === "last_24_hours") {
return { start: now - dayMs, end: now };
}
if (timeRange === "last_7_days") {
return { start: now - 7 * dayMs, end: now };
}
if (timeRange === "last_30_days") {
return { start: now - 30 * dayMs, end: now };
}
// Month/quarter bounds use UTC midnight for timezone-stable windows
if (timeRange === "current_month") {
const d = new Date();
const startUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
return { start: startUtc, end: now };
}
if (timeRange === "current_quarter") {
const d = new Date();
const qMonth = Math.floor(d.getUTCMonth() / 3) * 3;
const startUtc = Date.UTC(d.getUTCFullYear(), qMonth, 1);
return { start: startUtc, end: now };
}
}
return null;
}
/**
* Capture a request lifecycle record
*/
async trackRequest(record) {
// Preserve explicit costs (including 0). Only price when cost is omitted.
const cost = record.cost !== undefined
? record.cost
: calculateAdvancedCost(record.model, record.inputTokens, record.outputTokens, record.provider);
const fullRecord = {
...record,
id: crypto.randomUUID(),
cost,
};
await this.storage.saveRecord(fullRecord);
}
/**
* getProviderMetrics implementation
*/
async getProviderMetrics(options = {}) {
const allRecords = await this.storage.getRecords();
const range = this.parseTimeRange(options.timeRange);
// Filter records
const records = allRecords.filter((r) => {
if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
return false;
}
if (options.providers && options.providers.length > 0) {
if (!options.providers.includes(r.provider)) {
return false;
}
}
return true;
});
// Compute top-level aggregated metrics
let totalLatency = 0;
let totalTokens = 0;
let inputTokens = 0;
let outputTokens = 0;
let errors = 0;
let totalCost = 0;
const requestCount = records.length;
// Group by provider
const providerMap = {};
for (const r of records) {
totalLatency += r.latency;
totalTokens += r.totalTokens;
inputTokens += r.inputTokens;
outputTokens += r.outputTokens;
if (r.isError) {
errors++;
}
totalCost += r.cost;
if (!providerMap[r.provider]) {
providerMap[r.provider] = [];
}
providerMap[r.provider].push(r);
}
const averageLatency = requestCount > 0 ? totalLatency / requestCount : 0;
const errorRate = requestCount > 0 ? errors / requestCount : 0;
// Empty datasets must not report a fabricated 100% success rate
const successRate = requestCount > 0 ? 1 - errorRate : 0;
const costPerToken = totalTokens > 0 ? totalCost / totalTokens : 0;
// Build per-provider results
const providers = [];
for (const [name, pRecords] of Object.entries(providerMap)) {
const pCount = pRecords.length;
let pLatency = 0;
let pTotalTokens = 0;
let pInputTokens = 0;
let pOutputTokens = 0;
let pErrors = 0;
let pCost = 0;
for (const r of pRecords) {
pLatency += r.latency;
pTotalTokens += r.totalTokens;
pInputTokens += r.inputTokens;
pOutputTokens += r.outputTokens;
if (r.isError) {
pErrors++;
}
pCost += r.cost;
}
const pAvgLatency = pCount > 0 ? pLatency / pCount : 0;
const pErrorRate = pCount > 0 ? pErrors / pCount : 0;
const pSuccessRate = 1 - pErrorRate;
const pCostPerToken = pTotalTokens > 0 ? pCost / pTotalTokens : 0;
providers.push({
name,
averageLatency: pAvgLatency,
averageResponseTime: pAvgLatency, // docs compatibility
totalTokens: pTotalTokens,
inputTokens: pInputTokens,
outputTokens: pOutputTokens,
errorRate: pErrorRate,
successRate: pSuccessRate,
costPerToken: pCostPerToken,
totalCost: pCost,
requestCount: pCount,
});
}
return {
averageLatency,
averageResponseTime: averageLatency,
totalTokens,
inputTokens,
outputTokens,
errorRate,
successRate,
costPerToken,
totalCost,
requestCount,
providers,
};
}
/**
* Helper to format timestamps for date groupings.
* Clones the Date so week arithmetic never mutates the caller's value.
*/
formatGroupDate(timestamp, type) {
const d = new Date(timestamp);
if (type === "day") {
return d.toISOString().split("T")[0];
}
if (type === "month") {
return d.toISOString().substring(0, 7);
}
if (type === "week") {
// UTC Monday of the containing ISO week — never mutate caller input
// and avoid local/UTC drift from mixing getDay()/toISOString().
const clone = new Date(timestamp);
const day = clone.getUTCDay();
const diff = clone.getUTCDate() - day + (day === 0 ? -6 : 1);
clone.setUTCDate(diff);
return clone.toISOString().split("T")[0];
}
return d.toISOString().split("T")[0];
}
/**
* getCostAnalysis implementation
*/
async getCostAnalysis(options = {}) {
const allRecords = await this.storage.getRecords();
const range = this.parseTimeRange(options.timeRange);
const records = allRecords.filter((r) => {
if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
return false;
}
return true;
});
let totalCost = 0;
let totalTokens = 0;
let inputTokens = 0;
let outputTokens = 0;
const requestCount = records.length;
const groups = {};
const providerGroupsMap = {};
const groupByArr = Array.isArray(options.groupBy)
? options.groupBy
: typeof options.groupBy === "string"
? [options.groupBy]
: ["provider"];
for (const r of records) {
totalCost += r.cost;
totalTokens += r.totalTokens;
inputTokens += r.inputTokens;
outputTokens += r.outputTokens;
// Ensure per-provider items are always built for documentation support
if (!providerGroupsMap[r.provider]) {
providerGroupsMap[r.provider] = {
groupKey: r.provider,
provider: r.provider,
totalCost: 0,
costPerToken: 0,
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
requestCount: 0,
};
}
const pg = providerGroupsMap[r.provider];
pg.totalCost += r.cost;
pg.inputTokens += r.inputTokens;
pg.outputTokens += r.outputTokens;
pg.totalTokens += r.totalTokens;
pg.requestCount += 1;
pg.costPerToken = pg.totalTokens > 0 ? pg.totalCost / pg.totalTokens : 0;
// Build specific requested group key
const keyParts = [];
for (const g of groupByArr) {
if (g === "provider") {
keyParts.push(r.provider);
}
else if (g === "model") {
keyParts.push(r.model);
}
else if (g === "user_id" || g === "userId") {
keyParts.push(r.userId || "unknown");
}
else if (g === "day" || g === "week" || g === "month") {
keyParts.push(this.formatGroupDate(r.timestamp, g));
}
else {
keyParts.push(r.provider);
}
}
const groupKey = keyParts.join("#") || r.provider;
if (!groups[groupKey]) {
groups[groupKey] = {
groupKey,
provider: r.provider,
model: r.model,
userId: r.userId,
totalCost: 0,
costPerToken: 0,
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
requestCount: 0,
};
}
const gItem = groups[groupKey];
gItem.totalCost += r.cost;
gItem.inputTokens += r.inputTokens;
gItem.outputTokens += r.outputTokens;
gItem.totalTokens += r.totalTokens;
gItem.requestCount += 1;
gItem.costPerToken =
gItem.totalTokens > 0 ? gItem.totalCost / gItem.totalTokens : 0;
}
const providers = Object.values(providerGroupsMap);
// Optional future projections using simple average-based estimation
let projections;
if (options.includeProjections) {
if (records.length === 0) {
projections = { nextMonth: 0, nextQuarter: 0 };
}
else {
// Find span of records in days
const timestamps = records.map((r) => r.timestamp);
const minTime = Math.min(...timestamps);
const maxTime = Math.max(...timestamps);
const diffDays = Math.max(1, (maxTime - minTime) / (24 * 3600 * 1000));
const dailyAverage = totalCost / diffDays;
projections = {
nextMonth: dailyAverage * 30,
nextQuarter: dailyAverage * 90,
};
}
}
return {
totalCost,
totalTokens,
inputTokens,
outputTokens,
requestCount,
groups,
providers,
projections,
};
}
/**
* getTeamAnalytics implementation
*/
async getTeamAnalytics(options = {}) {
const allRecords = await this.storage.getRecords();
const range = this.parseTimeRange(options.timeRange);
// teamId and departments are independent AND filters (docs: both supported).
const records = allRecords.filter((r) => {
if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
return false;
}
if (options.teamId) {
const recTeam = r.teamId || "default";
if (recTeam !== options.teamId) {
return false;
}
}
if (options.departments && options.departments.length > 0) {
const dept = r.department;
if (!dept || !options.departments.includes(dept)) {
return false;
}
}
return true;
});
const totalRequests = records.length;
const uniqueUsersSet = new Set();
const providersSet = new Set();
const costBreakdownByProvider = {};
const costBreakdownByUser = {};
let totalQualityScore = 0;
let totalRelevance = 0;
let totalAccuracy = 0;
let totalCompleteness = 0;
let scoredCount = 0;
for (const r of records) {
if (r.userId) {
uniqueUsersSet.add(r.userId);
}
providersSet.add(r.provider);
costBreakdownByProvider[r.provider] =
(costBreakdownByProvider[r.provider] || 0) + r.cost;
const uKey = r.userId || "anonymous";
costBreakdownByUser[uKey] = (costBreakdownByUser[uKey] || 0) + r.cost;
if (r.qualityScore) {
scoredCount++;
totalQualityScore += r.qualityScore.overall;
totalRelevance += r.qualityScore.relevance;
totalAccuracy += r.qualityScore.accuracy;
totalCompleteness += r.qualityScore.completeness;
}
}
// Omit fabricated scores when no requests were evaluated
const qualityScores = scoredCount > 0
? {
overall: totalQualityScore / scoredCount,
relevance: totalRelevance / scoredCount,
accuracy: totalAccuracy / scoredCount,
completeness: totalCompleteness / scoredCount,
reasoning: "Aggregated automated response evaluations",
}
: undefined;
return {
totalRequests,
// Count only attributed users — never fabricate a user when none present
uniqueUsers: uniqueUsersSet.size,
providersUsed: Array.from(providersSet),
costBreakdownByProvider,
costBreakdownByUser,
qualityScores,
};
}
}