@scrubbe-auth/network-info
Version:
Network information and connection details for analytics
667 lines (660 loc) • 22.3 kB
JavaScript
class NetworkUtils {
static getConnectionTypeScore(connectionType) {
const scores = {
'ethernet': 30,
'wifi': 25,
'4g': 20,
'3g': 10,
'2g': 5,
'cellular': 15,
'bluetooth': 8,
'wimax': 12,
'unknown': 10
};
return scores[connectionType.toLowerCase()] || 10;
}
static getSpeedScore(speedMbps) {
if (speedMbps >= 100)
return 40; // 100+ Mbps
if (speedMbps >= 50)
return 35; // 50-100 Mbps
if (speedMbps >= 25)
return 30; // 25-50 Mbps
if (speedMbps >= 10)
return 25; // 10-25 Mbps
if (speedMbps >= 5)
return 20; // 5-10 Mbps
if (speedMbps >= 1)
return 15; // 1-5 Mbps
return 5; // < 1 Mbps
}
static getLatencyScore(latencyMs) {
if (latencyMs <= 20)
return 30; // Excellent
if (latencyMs <= 50)
return 25; // Good
if (latencyMs <= 100)
return 20; // Fair
if (latencyMs <= 200)
return 15; // Poor
return 5; // Very poor
}
static formatSpeed(speedMbps) {
if (speedMbps >= 1000) {
return `${(speedMbps / 1000).toFixed(1)} Gbps`;
}
return `${speedMbps.toFixed(1)} Mbps`;
}
static formatLatency(latencyMs) {
return `${Math.round(latencyMs)} ms`;
}
static getQualityDescription(quality) {
const descriptions = {
excellent: 'Excellent connection quality for all activities',
good: 'Good connection quality for most activities',
fair: 'Fair connection quality, may affect streaming/gaming',
poor: 'Poor connection quality, basic browsing only'
};
return descriptions[quality] || 'Unknown connection quality';
}
static isSlowConnection(connectionType, downloadSpeed) {
// Based on connection type
if (['2g', '3g', 'slow-2g'].includes(connectionType.toLowerCase())) {
return true;
}
// Based on speed
if (downloadSpeed !== undefined && downloadSpeed < 1) {
return true;
}
return false;
}
static isMobileConnection(connectionType) {
const mobileTypes = ['cellular', '2g', '3g', '4g', '5g', 'slow-2g'];
return mobileTypes.includes(connectionType.toLowerCase());
}
}
class Loggers {
constructor(debug = false, logLevel = 'info', outputs = [new ConsoleOutput()]) {
this.debugs = debug;
this.logLevel = logLevel;
this.outputs = outputs;
this.context = {};
}
setContext(context) {
this.context = { ...this.context, ...context };
}
clearContext() {
this.context = {};
}
debug(message, ...args) {
if (this.debugs && this.shouldLog('debug')) {
this.log('debug', message, args);
}
}
info(message, ...args) {
if (this.shouldLog('info')) {
this.log('info', message, args);
}
}
warn(message, ...args) {
if (this.shouldLog('warn')) {
this.log('warn', message, args);
}
}
error(message, ...args) {
if (this.shouldLog('error')) {
this.log('error', message, args);
}
}
shouldLog(level) {
const levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
return levels[level] >= levels[this.logLevel];
}
log(level, message, args) {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
args,
context: this.context
};
this.outputs.forEach(output => {
try {
output.write(logEntry);
}
catch (error) {
console.error('Failed to write log:', error);
}
});
}
// Create child logger with additional context
child(context) {
const child = new Loggers(this.debugs, this.logLevel, this.outputs);
child.setContext({ ...this.context, ...context });
return child;
}
// Add output
addOutput(output) {
this.outputs.push(output);
}
// Remove output
removeOutput(output) {
const index = this.outputs.indexOf(output);
if (index > -1) {
this.outputs.splice(index, 1);
}
}
}
class ConsoleOutput {
write(entry) {
const prefix = `[${entry.timestamp}] [${entry.level.toUpperCase()}] [Scrubbe Analytics]`;
const contextStr = Object.keys(entry.context).length > 0
? ` ${JSON.stringify(entry.context)}`
: '';
const fullMessage = `${prefix}${contextStr} ${entry.message}`;
switch (entry.level) {
case 'debug':
console.debug(fullMessage, ...entry.args);
break;
case 'info':
console.info(fullMessage, ...entry.args);
break;
case 'warn':
console.warn(fullMessage, ...entry.args);
break;
case 'error':
console.error(fullMessage, ...entry.args);
break;
}
}
}
class ConnectionCollector {
collect() {
const info = {
connectionType: 'unknown',
onLine: this.isOnline()
};
if (typeof navigator === 'undefined') {
return info;
}
// Get connection info from Network Information API
const connection = navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
if (connection) {
info.connectionType = connection.type || connection.effectiveType || 'unknown';
info.effectiveType = connection.effectiveType;
info.downlink = connection.downlink;
info.rtt = connection.rtt;
info.saveData = connection.saveData;
}
else {
// Fallback detection based on user agent and other indicators
info.connectionType = this.detectConnectionTypeFallback();
}
return info;
}
isOnline() {
if (typeof navigator !== 'undefined') {
return navigator.onLine;
}
return true;
}
detectConnectionTypeFallback() {
if (typeof navigator === 'undefined') {
return 'unknown';
}
const ua = navigator.userAgent.toLowerCase();
// Mobile detection
if (/mobile|android|iphone|ipad|phone/i.test(ua)) {
return 'cellular';
}
// Default to ethernet for desktop
return 'ethernet';
}
}
class SpeedTestCollector {
constructor(config) {
this.config = config;
}
async runSpeedTest() {
const startTime = Date.now();
// Run download speed test
const downloadSpeed = await this.measureDownloadSpeed();
// Measure latency
const latency = await this.measureLatency();
// Optionally measure upload speed (more resource intensive)
let uploadSpeed;
if (this.config.duration > 3000) {
uploadSpeed = await this.measureUploadSpeed();
}
return {
downloadSpeed,
uploadSpeed,
latency,
timestamp: Date.now(),
testDuration: Date.now() - startTime
};
}
async measureDownloadSpeed() {
const testUrl = this.config.testUrl || this.getSpeedTestUrl();
const iterations = 3;
const results = [];
for (let i = 0; i < iterations; i++) {
try {
const result = await this.singleDownloadTest(testUrl);
results.push(result);
}
catch (error) {
console.warn(`Download test ${i + 1} failed:`, error);
}
}
if (results.length === 0) {
throw new Error('All download tests failed');
}
// Return median result to avoid outliers
results.sort((a, b) => a - b);
return results[Math.floor(results.length / 2)];
}
async singleDownloadTest(url) {
const startTime = performance.now();
const response = await fetch(url, {
method: 'GET',
cache: 'no-cache',
signal: AbortSignal.timeout(this.config.timeout)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const buffer = await response.arrayBuffer();
const endTime = performance.now();
const duration = (endTime - startTime) / 1000; // seconds
const bytes = buffer.byteLength;
const bitsPerSecond = (bytes * 8) / duration;
const mbps = bitsPerSecond / (1024 * 1024);
return mbps;
}
async measureUploadSpeed() {
try {
// Create test payload (1MB)
const testData = new ArrayBuffer(1024 * 1024);
const startTime = performance.now();
const response = await fetch('/api/speed-test/upload', {
method: 'POST',
body: testData,
signal: AbortSignal.timeout(this.config.timeout)
});
const endTime = performance.now();
if (!response.ok) {
throw new Error(`Upload test failed: ${response.status}`);
}
const duration = (endTime - startTime) / 1000;
const bitsPerSecond = (testData.byteLength * 8) / duration;
return bitsPerSecond / (1024 * 1024);
}
catch (error) {
console.warn('Upload speed test failed:', error);
return 0;
}
}
async measureLatency() {
const iterations = 5;
const results = [];
for (let i = 0; i < iterations; i++) {
try {
const start = performance.now();
await fetch(this.getPingUrl(), {
method: 'HEAD',
mode: 'no-cors',
cache: 'no-cache'
});
const end = performance.now();
results.push(end - start);
}
catch {
// Ignore failed pings
}
}
if (results.length === 0) {
return -1;
}
// Return average latency
return results.reduce((a, b) => a + b, 0) / results.length;
}
getSpeedTestUrl() {
// Use different sized test files based on expected connection speed
const sizes = ['1MB', '5MB', '10MB'];
const selectedSize = sizes[0]; // Start with smallest
return `https://speed.cloudflare.com/__down?bytes=${this.getMBInBytes(selectedSize)}`;
}
getPingUrl() {
return 'https://www.google.com/favicon.ico';
}
getMBInBytes(size) {
const mb = parseInt(size.replace('MB', ''));
return mb * 1024 * 1024;
}
}
class IPInfoCollector {
constructor(timeout = 5000) {
this.timeout = timeout;
}
async collect() {
const services = [
() => this.collectFromIPAPI(),
() => this.collectFromIPGeolocation(),
() => this.collectFromIPInfo()
];
for (const service of services) {
try {
const result = await service();
if (result) {
return result;
}
}
catch (error) {
console.warn('IP info service failed:', error);
}
}
return null;
}
async collectFromIPAPI() {
try {
const response = await fetch('https://ipapi.co/json/', {
signal: AbortSignal.timeout(this.timeout)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return {
ip: data.ip,
isp: data.org,
organization: data.org,
asn: data.asn,
country: data.country_name,
region: data.region,
city: data.city,
// Basic VPN/proxy detection based on known indicators
vpn: this.isVPN(data.org),
proxy: this.isProxy(data.org),
hosting: this.isHosting(data.org)
};
}
catch (error) {
throw new Error(`ipapi.co failed: ${error}`);
}
}
async collectFromIPGeolocation() {
try {
const response = await fetch('https://api.ipgeolocation.io/ipgeo', {
signal: AbortSignal.timeout(this.timeout)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return {
ip: data.ip,
isp: data.isp,
organization: data.organization,
asn: data.asn,
country: data.country_name,
region: data.state_prov,
city: data.city,
vpn: this.isVPN(data.isp),
proxy: this.isProxy(data.isp),
hosting: this.isHosting(data.isp)
};
}
catch (error) {
throw new Error(`ipgeolocation.io failed: ${error}`);
}
}
async collectFromIPInfo() {
try {
const response = await fetch('https://ipinfo.io/json', {
signal: AbortSignal.timeout(this.timeout)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return {
ip: data.ip,
isp: data.org,
organization: data.org,
country: data.country,
region: data.region,
city: data.city,
vpn: this.isVPN(data.org),
proxy: this.isProxy(data.org),
hosting: this.isHosting(data.org)
};
}
catch (error) {
throw new Error(`ipinfo.io failed: ${error}`);
}
}
isVPN(org) {
if (!org)
return false;
const vpnIndicators = [
'vpn', 'proxy', 'tor', 'mullvad', 'nordvpn', 'expressvpn',
'protonvpn', 'surfshark', 'cyberghost', 'privatevpn'
];
return vpnIndicators.some(indicator => org.toLowerCase().includes(indicator));
}
isProxy(org) {
if (!org)
return false;
const proxyIndicators = [
'proxy', 'cloudflare', 'fastly', 'akamai', 'maxcdn'
];
return proxyIndicators.some(indicator => org.toLowerCase().includes(indicator));
}
isHosting(org) {
if (!org)
return false;
const hostingIndicators = [
'amazon', 'aws', 'google cloud', 'microsoft azure',
'digitalocean', 'linode', 'vultr', 'hetzner', 'ovh'
];
return hostingIndicators.some(indicator => org.toLowerCase().includes(indicator));
}
}
class NetworkInfo {
constructor(config = {}) {
this.config = {
enableSpeedTest: config.enableSpeedTest ?? false,
enableConnectionType: config.enableConnectionType ?? true,
enableISP: config.enableISP ?? false,
enableVPNDetection: config.enableVPNDetection ?? false,
enableQualityMetrics: config.enableQualityMetrics ?? true,
speedTestDuration: config.speedTestDuration || 5000,
speedTestUrl: config.speedTestUrl,
cacheTimeout: config.cacheTimeout || 300000, // 5 minutes
debug: config.debug ?? false,
timeout: config.timeout || 10000,
...config
};
this.logger = new Loggers(this.config.debug);
this.connectionCollector = new ConnectionCollector();
this.speedTestCollector = new SpeedTestCollector({
duration: this.config.speedTestDuration,
testUrl: this.config.speedTestUrl,
timeout: this.config.timeout
});
this.ipInfoCollector = new IPInfoCollector(this.config.timeout);
}
async getNetworkInfo() {
// Check cache
if (this.isCacheValid()) {
this.logger.debug('Returning cached network info');
return this.cachedInfo;
}
try {
const networkInfo = await this.collectNetworkInfo();
// Cache the result
this.cachedInfo = networkInfo;
this.cacheTimestamp = Date.now();
this.logger.debug('Network info collected successfully', networkInfo);
return networkInfo;
}
catch (error) {
this.logger.error('Failed to collect network info:', error);
return this.getBasicNetworkInfo();
}
}
async collectNetworkInfo() {
const networkInfo = {
timestamp: Date.now(),
connectionType: 'unknown'
};
// Collect connection type and basic info
if (this.config.enableConnectionType) {
const connectionInfo = this.connectionCollector.collect();
Object.assign(networkInfo, connectionInfo);
}
// Collect IP and ISP info
if (this.config.enableISP || this.config.enableVPNDetection) {
try {
const ipInfo = await this.ipInfoCollector.collect();
if (ipInfo) {
networkInfo.ip = ipInfo.ip;
networkInfo.isp = ipInfo.isp;
networkInfo.organization = ipInfo.organization;
networkInfo.asn = ipInfo.asn;
networkInfo.vpn = ipInfo.vpn;
networkInfo.proxy = ipInfo.proxy;
networkInfo.hosting = ipInfo.hosting;
}
}
catch (error) {
this.logger.warn('Failed to collect IP info:', error);
}
}
// Perform speed test
if (this.config.enableSpeedTest) {
try {
const speedResult = await this.speedTestCollector.runSpeedTest();
networkInfo.downloadSpeed = speedResult.downloadSpeed;
networkInfo.uploadSpeed = speedResult.uploadSpeed;
networkInfo.latency = speedResult.latency;
networkInfo.jitter = speedResult.jitter;
}
catch (error) {
this.logger.warn('Speed test failed:', error);
}
}
// Calculate quality metrics
if (this.config.enableQualityMetrics) {
networkInfo.quality = this.calculateNetworkQuality(networkInfo);
}
return networkInfo;
}
getBasicNetworkInfo() {
return {
timestamp: Date.now(),
connectionType: this.connectionCollector.collect().connectionType || 'unknown'
};
}
calculateNetworkQuality(info) {
let score = 0;
let maxScore = 0;
// Connection type scoring
maxScore += 30;
const connectionScore = NetworkUtils.getConnectionTypeScore(info.connectionType);
score += connectionScore;
// Speed scoring (if available)
if (info.downloadSpeed !== undefined) {
maxScore += 40;
const speedScore = NetworkUtils.getSpeedScore(info.downloadSpeed);
score += speedScore;
}
// Latency scoring (if available)
if (info.latency !== undefined) {
maxScore += 30;
const latencyScore = NetworkUtils.getLatencyScore(info.latency);
score += latencyScore;
}
const percentage = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
let quality;
if (percentage >= 80)
quality = 'excellent';
else if (percentage >= 60)
quality = 'good';
else if (percentage >= 40)
quality = 'fair';
else
quality = 'poor';
return quality;
}
isCacheValid() {
if (!this.cachedInfo || !this.cacheTimestamp) {
return false;
}
return Date.now() - this.cacheTimestamp < this.config.cacheTimeout;
}
// Specific test methods
async runSpeedTest() {
if (!this.config.enableSpeedTest) {
this.logger.warn('Speed test is disabled');
return null;
}
try {
return await this.speedTestCollector.runSpeedTest();
}
catch (error) {
this.logger.error('Speed test failed:', error);
return null;
}
}
async getConnectionInfo() {
return this.connectionCollector.collect();
}
async getIPInfo() {
return await this.ipInfoCollector.collect();
}
// Utility methods
clearCache() {
this.cachedInfo = undefined;
this.cacheTimestamp = undefined;
this.logger.debug('Network info cache cleared');
}
updateConfig(config) {
Object.assign(this.config, config);
this.logger.debug('Configuration updated');
}
getConfig() {
return { ...this.config };
}
// Static utility methods
static isOnline() {
if (typeof navigator !== 'undefined') {
return navigator.onLine;
}
return true; // Assume online if navigator not available
}
static getConnectionType() {
return new ConnectionCollector().collect().connectionType || 'unknown';
}
static async measureLatency(url = 'https://www.google.com/favicon.ico') {
try {
const start = performance.now();
await fetch(url, { method: 'HEAD', mode: 'no-cors' });
const end = performance.now();
return Math.round(end - start);
}
catch {
return -1;
}
}
}
export { NetworkInfo, NetworkUtils, NetworkInfo as default };
//# sourceMappingURL=index.esm.js.map