homebridge-mopar
Version:
Homebridge plugin for Mopar vehicles (Chrysler, Dodge, Jeep, Ram, Fiat, Alfa Romeo) with Uconnect
268 lines (238 loc) • 7.44 kB
JavaScript
/**
* Local Metrics Collector
*
* PRIVACY GUARANTEE: All metrics stay on the user's local machine.
* NO external calls, NO tracking, NO data transmission.
*
* Purpose: Help users debug issues by tracking local statistics.
* Metrics are only visible when debug logging is enabled.
*/
class Metrics {
constructor() {
this.startTime = Date.now();
this.commands = new Map(); // command -> { success: 0, failure: 0, totalTime: 0 }
this.apiCalls = new Map(); // endpoint -> { count: 0, errors: 0 }
this.errors = new Map(); // error type -> count
this.sessionRefreshes = { success: 0, failure: 0 };
this.cookieRefreshes = { success: 0, failure: 0 };
this.logins = { success: 0, failure: 0 };
}
/**
* Record a command execution
* @param {string} command - Command type (lock, unlock, start, etc.)
* @param {boolean} success - Whether command succeeded
* @param {number} duration - Execution time in milliseconds
*/
recordCommand(command, success, duration = 0) {
if (!this.commands.has(command)) {
this.commands.set(command, { success: 0, failure: 0, totalTime: 0, count: 0 });
}
const stats = this.commands.get(command);
stats.count++;
stats.totalTime += duration;
if (success) {
stats.success++;
} else {
stats.failure++;
}
}
/**
* Record an API call
* @param {string} endpoint - API endpoint name
* @param {boolean} success - Whether call succeeded
*/
recordAPICall(endpoint, success) {
if (!this.apiCalls.has(endpoint)) {
this.apiCalls.set(endpoint, { count: 0, errors: 0 });
}
const stats = this.apiCalls.get(endpoint);
stats.count++;
if (!success) {
stats.errors++;
}
}
/**
* Record an error occurrence
* @param {string} errorType - Type of error (e.g., 'NETWORK', 'AUTH', 'TIMEOUT')
*/
recordError(errorType) {
const count = this.errors.get(errorType) || 0;
this.errors.set(errorType, count + 1);
}
/**
* Record a session refresh attempt
* @param {boolean} success - Whether refresh succeeded
*/
recordSessionRefresh(success) {
if (success) {
this.sessionRefreshes.success++;
} else {
this.sessionRefreshes.failure++;
}
}
/**
* Record a cookie refresh attempt
* @param {boolean} success - Whether refresh succeeded
*/
recordCookieRefresh(success) {
if (success) {
this.cookieRefreshes.success++;
} else {
this.cookieRefreshes.failure++;
}
}
/**
* Record a login attempt
* @param {boolean} success - Whether login succeeded
*/
recordLogin(success) {
if (success) {
this.logins.success++;
} else {
this.logins.failure++;
}
}
/**
* Get summary statistics
* @returns {object} Summary of all metrics
*/
getSummary() {
const uptime = Date.now() - this.startTime;
const uptimeMinutes = Math.floor(uptime / 60000);
const uptimeHours = Math.floor(uptime / 3600000);
return {
uptime: {
milliseconds: uptime,
minutes: uptimeMinutes,
hours: uptimeHours,
formatted: this.formatUptime(uptime),
},
commands: this.getCommandStats(),
logins: this.logins,
sessionRefreshes: this.sessionRefreshes,
cookieRefreshes: this.cookieRefreshes,
apiCalls: this.getAPICallStats(),
errors: this.getErrorStats(),
};
}
/**
* Get command statistics
* @returns {object} Command stats by type
*/
getCommandStats() {
const stats = {};
this.commands.forEach((value, key) => {
const avgTime = value.count > 0 ? Math.round(value.totalTime / value.count) : 0;
const successRate = value.count > 0 ? Math.round((value.success / value.count) * 100) : 0;
stats[key] = {
total: value.count,
success: value.success,
failure: value.failure,
successRate: `${successRate}%`,
avgDuration: `${avgTime}ms`,
};
});
return stats;
}
/**
* Get API call statistics
* @returns {object} API call stats by endpoint
*/
getAPICallStats() {
const stats = {};
this.apiCalls.forEach((value, key) => {
const errorRate = value.count > 0 ? Math.round((value.errors / value.count) * 100) : 0;
stats[key] = {
total: value.count,
errors: value.errors,
errorRate: `${errorRate}%`,
};
});
return stats;
}
/**
* Get error statistics
* @returns {object} Error counts by type
*/
getErrorStats() {
const stats = {};
this.errors.forEach((value, key) => {
stats[key] = value;
});
return stats;
}
/**
* Format uptime in human-readable format
* @param {number} milliseconds - Uptime in milliseconds
* @returns {string} Formatted uptime
*/
formatUptime(milliseconds) {
const hours = Math.floor(milliseconds / 3600000);
const minutes = Math.floor((milliseconds % 3600000) / 60000);
const seconds = Math.floor((milliseconds % 60000) / 1000);
if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds}s`;
} else {
return `${seconds}s`;
}
}
/**
* Log metrics summary (only in debug mode)
* @param {function} log - Logger function
*/
logSummary(log) {
const summary = this.getSummary();
log('[METRICS] ========================================');
log(`[METRICS] Plugin Uptime: ${summary.uptime.formatted}`);
log('[METRICS] ========================================');
// Command statistics
if (Object.keys(summary.commands).length > 0) {
log('[METRICS] Command Statistics:');
Object.entries(summary.commands).forEach(([cmd, stats]) => {
log(`[METRICS] ${cmd}: ${stats.total} total, ${stats.successRate} success, avg ${stats.avgDuration}`);
});
}
// Login statistics
const totalLogins = summary.logins.success + summary.logins.failure;
if (totalLogins > 0) {
log(`[METRICS] Logins: ${summary.logins.success} success, ${summary.logins.failure} failure`);
}
// Session refresh statistics
const totalSessionRefreshes = summary.sessionRefreshes.success + summary.sessionRefreshes.failure;
if (totalSessionRefreshes > 0) {
log(
`[METRICS] Session Refreshes: ${summary.sessionRefreshes.success} success, ${summary.sessionRefreshes.failure} failure`
);
}
// Cookie refresh statistics
const totalCookieRefreshes = summary.cookieRefreshes.success + summary.cookieRefreshes.failure;
if (totalCookieRefreshes > 0) {
log(
`[METRICS] Cookie Refreshes: ${summary.cookieRefreshes.success} success, ${summary.cookieRefreshes.failure} failure`
);
}
// Error statistics
if (Object.keys(summary.errors).length > 0) {
log('[METRICS] Error Counts:');
Object.entries(summary.errors).forEach(([type, count]) => {
log(`[METRICS] ${type}: ${count}`);
});
}
log('[METRICS] ========================================');
}
/**
* Reset all metrics
*/
reset() {
this.startTime = Date.now();
this.commands.clear();
this.apiCalls.clear();
this.errors.clear();
this.sessionRefreshes = { success: 0, failure: 0 };
this.cookieRefreshes = { success: 0, failure: 0 };
this.logins = { success: 0, failure: 0 };
}
}
module.exports = Metrics;