user-analytics-tracker
v4.7.0
Published
Comprehensive analytics tracking library with device detection, network analysis, location tracking, and IP geolocation
Maintainers
Readme
user-analytics-tracker
A comprehensive, lightweight analytics tracking library for React applications. Track device information, network type, user location, attribution data, and more—all with zero runtime dependencies (React as peer dependency).
🔒 Privacy-First & Self-Hosted: All analytics data is sent to your own backend server. No data is sent to third-party servers. You have full control over your analytics data.
✨ Features
- 🔍 Device Detection: Automatically detects device type, OS, browser, model, brand, and hardware specs using User-Agent Client Hints
- 🌐 Network & Connection Info: Accurate ISP and connection details from ipwho.is API (ASN, organization, domain)
- 📍 Location Tracking:
- IP-based location - Requires user consent (privacy-compliant)
- GPS location - Requires explicit user consent and browser permission
- Includes public IP address, country, city, region, timezone, continent, flag, connection details
- Dynamic key storage: All IP location API fields are automatically captured
- Automatic fallback from GPS to IP when GPS unavailable
- Consent management utilities included
- 🎯 Attribution Tracking: UTM parameters, referrer tracking, first/last touch attribution
- 📊 IP Geolocation: Client-side and server-side IP-based location detection utilities
- 🔒 Privacy-First: User consent required for location tracking (GPS & IP), consent management utilities
- 🎯 Custom Event Tracking: Firebase/Google Analytics-style event tracking with automatic context collection
- ⚡ Event Batching & Queue System: Automatic event batching reduces API calls by 50-90%. Events are queued and sent in configurable batches with offline persistence
- 🔄 Retry Logic: Automatic retry with exponential backoff for failed requests. Configurable retry attempts and delays
- 📝 Enhanced Logging: Configurable log levels (silent, error, warn, info, debug) with automatic dev/prod level selection
- 🔌 Plugin System: Extensible plugin architecture for event transformation, filtering, and enrichment
- 📈 Session Management: Enhanced session tracking with timeout detection and automatic renewal
- 🐛 Debug Tools: Built-in debugging utilities for development (queue inspection, manual flush, stats)
- 📊 Performance Metrics: Optional metrics collection for monitoring events, retries, and performance
- ⚡ Lightweight: Zero runtime dependencies (except React)
- 📦 TypeScript: Fully typed with comprehensive type definitions
- 🎨 Framework Agnostic Core: Core detectors work without React
- 🧪 Well Tested: Comprehensive test suite with Vitest
📦 Installation
npm install user-analytics-tracker react react-dom
# or
yarn add user-analytics-tracker react react-dom
# or
pnpm add user-analytics-tracker react react-domNote: React and React-DOM are peer dependencies and must be installed separately.
🔒 Self-Hosted Analytics - Configure Your Backend URL
All analytics data is sent to YOUR backend server - no third-party servers involved. You have complete control over your data.
Quick Configuration
Simply provide your backend URL in the apiEndpoint configuration:
import { useAnalytics } from 'user-analytics-tracker';
function App() {
const analytics = useAnalytics({
config: {
apiEndpoint: 'https://api.yourcompany.com/analytics', // Your backend URL
},
});
}Advanced Configuration Options
The package now supports extensive configuration options for batching, retry logic, logging, and more:
const analytics = useAnalytics({
config: {
apiEndpoint: 'https://api.yourcompany.com/analytics',
// Event batching configuration
batchSize: 10, // Events per batch (default: 10)
batchInterval: 5000, // Flush interval in ms (default: 5000)
maxQueueSize: 100, // Max queued events (default: 100)
// Retry configuration
maxRetries: 3, // Max retry attempts (default: 3)
retryDelay: 1000, // Initial retry delay in ms (default: 1000)
// Session configuration
sessionTimeout: 1800000, // Session timeout in ms (default: 30 min)
// Logging configuration
logLevel: 'warn', // 'silent' | 'error' | 'warn' | 'info' | 'debug' (default: 'warn')
// Metrics configuration
enableMetrics: false, // Enable metrics collection (default: false)
// IP Geolocation - optional. For paid users in browser use proxyUrl so API key is not exposed (see docs)
ipGeolocation: {
proxyUrl: '/api/ip-geolocation', // recommended for paid: browser → your backend → ipwho.is (key on server only)
// Or direct: apiKey, baseUrl, ip (server-side). Do not put apiKey in client when using proxyUrl.
timeout: 5000,
},
// Field storage configuration (optional) - control which fields are stored
fieldStorage: {
ipLocation: { mode: 'essential' }, // IP location fields (includes connection data)
deviceInfo: { mode: 'essential' }, // Device info fields
// networkInfo: Not stored in essential mode - use connection from ipwho.is instead
location: { mode: 'essential' }, // Location fields
attribution: { mode: 'essential' }, // Attribution fields
// Each can be: 'essential' (default) | 'all' | 'custom'
// For 'custom': specify fields array
// For 'all': specify exclude array
},
// Legacy: IP Location storage (backward compatible)
ipLocationFields: { mode: 'essential' },
},
});Configuration Options
You can configure your backend URL in three ways:
1. Full URL (Recommended for Production)
Use a complete URL pointing to your backend server:
const analytics = useAnalytics({
config: {
// Point to your own server
apiEndpoint: 'https://api.yourcompany.com/analytics',
// Or with a custom port
// apiEndpoint: 'https://api.yourcompany.com:8080/analytics',
// Or using a subdomain
// apiEndpoint: 'https://analytics.yourcompany.com/track',
},
});2. Relative Path (Same Domain)
Use a relative path if your API is on the same domain as your frontend:
const analytics = useAnalytics({
config: {
// Sends to: https://yourdomain.com/api/analytics
apiEndpoint: '/api/analytics',
},
});3. Environment Variables (Best Practice)
Use environment variables for different environments:
// .env.local (development)
// NEXT_PUBLIC_ANALYTICS_API=https://api-dev.yourcompany.com/analytics
// NEXT_PUBLIC_IPWHOIS_API_KEY=your-ipwho-is-key // optional, for higher rate limits
// .env.production
// NEXT_PUBLIC_ANALYTICS_API=https://api.yourcompany.com/analytics
// NEXT_PUBLIC_IPWHOIS_API_KEY=your-ipwho-is-key
const analytics = useAnalytics({
config: {
apiEndpoint: process.env.NEXT_PUBLIC_ANALYTICS_API || '/api/analytics',
// Optional: your own ipwho.is API key (use env var; omit for free tier)
ipGeolocation: {
apiKey: process.env.NEXT_PUBLIC_IPWHOIS_API_KEY,
timeout: 5000,
},
},
});Step-by-Step Setup
- Set up your backend API endpoint (see Backend Setup below)
- Configure the frontend with your backend URL
- Test the connection using browser DevTools Network tab
Examples by Framework
React (Create React App)
// src/App.tsx
import { useAnalytics } from 'user-analytics-tracker';
function App() {
const analytics = useAnalytics({
config: {
apiEndpoint: process.env.REACT_APP_ANALYTICS_API || 'https://api.yourcompany.com/analytics',
},
});
}Next.js
// app/layout.tsx or pages/_app.tsx
import { useAnalytics } from 'user-analytics-tracker';
export default function Layout() {
useAnalytics({
config: {
apiEndpoint: process.env.NEXT_PUBLIC_ANALYTICS_API || '/api/analytics',
},
});
}Vite + React
// src/main.tsx
import { useAnalytics } from 'user-analytics-tracker';
function App() {
useAnalytics({
config: {
apiEndpoint: import.meta.env.VITE_ANALYTICS_API || 'https://api.yourcompany.com/analytics',
},
});
}🚀 Quick Start
Basic Usage (React Hook)
import { useAnalytics } from 'user-analytics-tracker';
function MyApp() {
const {
sessionId,
deviceInfo,
location,
trackEvent,
trackPageView
} = useAnalytics({
autoSend: true,
config: {
// Use your own backend server (full URL)
apiEndpoint: 'https://api.yourcompany.com/analytics',
// Or use relative path (same domain)
// apiEndpoint: '/api/analytics',
},
});
// Track page view on mount
useEffect(() => {
trackPageView();
}, [trackPageView]);
const handleButtonClick = async () => {
// Track custom event (Firebase/GA-style)
await trackEvent('button_click', {
button_name: 'signup',
button_location: 'header'
});
};
return (
<div>
<p>Device: {deviceInfo?.deviceBrand} {deviceInfo?.deviceModel}</p>
{/* Connection data from ipwho.is (in customData.ipLocation.connection) */}
<button onClick={handleButtonClick}>
Track Click
</button>
</div>
);
}Standalone Detectors (No React)
import {
NetworkDetector,
DeviceDetector,
AttributionDetector,
LocationDetector,
} from 'user-analytics-tracker';
// Detect network type
const network = NetworkDetector.detect();
console.log(network.type); // 'wifi' | 'cellular' | 'hotspot' | 'ethernet' | 'unknown'
// Detect device info
const device = await DeviceDetector.detect();
console.log(device.deviceBrand, device.deviceModel);
// Detect attribution (UTM params, referrer, etc.)
const attribution = AttributionDetector.detect();
console.log(attribution.utm_source);
// Detect location (automatic IP-based if no consent, GPS if consent granted)
const location = await LocationDetector.detect();
console.log(location.lat, location.lon);
console.log(location.ip); // Public IP (when using IP-based location)
console.log(location.country, location.city); // Location details
// Or get IP-based location only (no permission needed)
const ipLocation = await LocationDetector.detectIPOnly();
console.log(ipLocation.ip, ipLocation.country, ipLocation.city);📚 API Reference
React Hook: useAnalytics
The main React hook for analytics tracking.
Parameters
useAnalytics(options?: UseAnalyticsOptions): UseAnalyticsReturnOptions:
interface UseAnalyticsOptions {
autoSend?: boolean; // Auto-send analytics on mount (default: true)
config?: Partial<AnalyticsConfig>;
onReady?: (data: {
sessionId: string;
networkInfo: NetworkInfo;
deviceInfo: DeviceInfo;
location: LocationInfo;
attribution: AttributionInfo;
}) => void; // Callback when data is ready
}
interface AnalyticsConfig {
apiEndpoint: string;
// Batching options
batchSize?: number; // Events per batch (default: 10)
batchInterval?: number; // Flush interval in ms (default: 5000)
maxQueueSize?: number; // Max queued events (default: 100)
// Retry options
maxRetries?: number; // Max retry attempts (default: 3)
retryDelay?: number; // Initial retry delay in ms (default: 1000)
// Session options
sessionTimeout?: number; // Session timeout in ms (default: 1800000 = 30 min)
// Logging options
logLevel?: LogLevel; // 'silent' | 'error' | 'warn' | 'info' | 'debug' (default: 'warn')
// Metrics options
enableMetrics?: boolean; // Enable metrics collection (default: false)
// IP Geolocation (ipwho.is) - pass your own API key via env var for higher rate limits
ipGeolocation?: {
apiKey?: string; // Use env var. Do not use in browser if using proxyUrl.
baseUrl?: string; // Default: 'https://ipwho.is'. Ignored when proxyUrl is set.
timeout?: number; // Default: 5000
ip?: string; // When provided (server-side), lookup this IP. Ignored when proxyUrl is set.
proxyUrl?: string; // For paid users in browser: client calls this; backend calls ipwho.is with API key (key never in client).
};
// Existing options
autoSend?: boolean;
enableLocation?: boolean;
enableIPGeolocation?: boolean;
enableNetworkDetection?: boolean;
enableDeviceDetection?: boolean;
enableAttribution?: boolean;
sessionStoragePrefix?: string;
localStoragePrefix?: string;
}Returns
interface UseAnalyticsReturn {
sessionId: string | null;
networkInfo: NetworkInfo | null;
deviceInfo: DeviceInfo | null;
location: LocationInfo | null;
attribution: AttributionInfo | null;
pageVisits: number;
interactions: number;
logEvent: (customData?: Record<string, any>) => Promise<void>;
trackEvent: (eventName: string, parameters?: Record<string, any>) => Promise<void>;
trackPageView: (pageName?: string, parameters?: Record<string, any>) => Promise<void>;
incrementInteraction: () => void;
refresh: () => Promise<{
net: NetworkInfo;
dev: DeviceInfo;
attr: AttributionInfo;
loc: LocationInfo;
}>;
}Detectors
NetworkDetector.detect()
Detects network connection type and quality.
const network = NetworkDetector.detect();
// Returns:
// {
// type: 'wifi' | 'cellular' | 'hotspot' | 'ethernet' | 'unknown';
// effectiveType?: string; // '2g', '3g', '4g', etc.
// downlink?: number; // Mbps
// rtt?: number; // ms
// saveData?: boolean;
// connectionType?: string;
// }DeviceDetector.detect()
Detects device information (async - uses User-Agent Client Hints).
const device = await DeviceDetector.detect();
// Returns:
// {
// type: 'mobile' | 'tablet' | 'desktop' | 'unknown';
// os: string;
// osVersion: string;
// browser: string;
// browserVersion: string;
// deviceModel: string;
// deviceBrand: string;
// screenResolution: string;
// // ... more fields
// }LocationDetector.detect()
Detects location (IP-first when no consent, GPS when consent granted). Automatically falls back to IP if GPS fails.
const location = await LocationDetector.detect();
// Returns:
// {
// lat?: number | null;
// lon?: number | null;
// accuracy?: number | null; // GPS only
// permission: 'granted' | 'denied' | 'prompt' | 'unsupported';
// source: 'gps' | 'ip' | 'unknown';
// ts?: string;
// // IP-based location includes:
// ip?: string | null; // Public IP address
// country?: string; // Country name
// countryCode?: string; // ISO country code
// city?: string; // City name
// region?: string; // Region/state
// timezone?: string; // Timezone
// }LocationDetector.detectIPOnly()
Get IP-based location only (fast, automatic, no permission needed).
const location = await LocationDetector.detectIPOnly();
// Returns IP-based location with IP address, country, city, coordinates
// Works immediately without user permissionLocationDetector.detectWithAutoConsent()
Automatically grants consent and tries GPS, falls back to IP if GPS fails.
const location = await LocationDetector.detectWithAutoConsent();
// 1. Automatically grants location consent
// 2. Tries GPS location (if available)
// 3. Falls back to IP-based location if GPS fails/denied/unavailablegetPublicIP()
Get just the public IP address (utility function).
import { getPublicIP } from 'user-analytics-tracker';
const ip = await getPublicIP();
console.log(ip); // "203.0.113.42"AttributionDetector.detect()
Detects UTM parameters, referrer, and session tracking.
const attribution = AttributionDetector.detect();
// Returns:
// {
// landingUrl: string;
// referrerUrl: string | null;
// referrerDomain: string | null;
// utm_source?: string | null;
// utm_medium?: string | null;
// utm_campaign?: string | null;
// // ... more UTM fields
// firstTouch?: Record<string, string | null> | null;
// lastTouch?: Record<string, string | null> | null;
// sessionStart?: string | null;
// }Services
AnalyticsService.configure()
Configure the analytics service with advanced options.
import { AnalyticsService } from 'user-analytics-tracker';
AnalyticsService.configure({
apiEndpoint: 'https://api.yourcompany.com/analytics',
batchSize: 20, // Events per batch (default: 10)
batchInterval: 10000, // Flush interval in ms (default: 5000)
maxQueueSize: 100, // Max queued events (default: 100)
maxRetries: 5, // Max retry attempts (default: 3)
retryDelay: 2000, // Initial retry delay in ms (default: 1000)
sessionTimeout: 1800000, // Session timeout in ms (default: 30 min)
logLevel: 'info', // Logging verbosity (default: 'warn')
enableMetrics: true, // Enable metrics collection (default: false)
});AnalyticsService.trackUserJourney()
Send analytics data to your backend.
import { AnalyticsService } from 'user-analytics-tracker';
// Configure endpoint - use your own server
AnalyticsService.configure({
apiEndpoint: 'https://api.yourcompany.com/analytics'
});
// Or use relative path (same domain)
// AnalyticsService.configure({ apiEndpoint: '/api/analytics' });
// Track event
await AnalyticsService.trackUserJourney({
sessionId: 'abc123',
pageUrl: 'https://example.com/page',
networkInfo: network,
deviceInfo: device,
location: location,
attribution: attribution,
customData: { userId: 'user123', action: 'purchase' },
});AnalyticsService.flushQueue()
Manually flush the event queue (useful before page unload).
// Flush all queued events immediately
await AnalyticsService.flushQueue();AnalyticsService.getQueueSize()
Get the current number of events in the queue.
const size = AnalyticsService.getQueueSize();
console.log(`Queue has ${size} events`);AnalyticsService.getMetrics()
Get performance metrics (if enabled).
const metrics = AnalyticsService.getMetrics();
if (metrics) {
console.log(`Sent: ${metrics.eventsSent}, Failed: ${metrics.eventsFailed}`);
}Utilities
Logger
Configure logging levels for better debugging and production use.
import { logger } from 'user-analytics-tracker';
// Set log level
logger.setLevel('debug'); // 'silent' | 'error' | 'warn' | 'info' | 'debug'
// Use logger
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message');Plugin Manager
Register and manage plugins for event transformation.
import { pluginManager } from 'user-analytics-tracker';
// Register a plugin
pluginManager.register({
name: 'my-plugin',
beforeSend: (event) => {
// Transform event
return event;
},
});
// Unregister a plugin
pluginManager.unregister('my-plugin');
// Get all plugins
const plugins = pluginManager.getPlugins();Queue Manager
Advanced queue management (for power users).
import { QueueManager } from 'user-analytics-tracker';
const queue = new QueueManager({
batchSize: 20,
batchInterval: 10000,
maxQueueSize: 200,
storageKey: 'my-queue',
});
queue.setFlushCallback(async (events) => {
// Custom flush logic
});Metrics Collector
Collect and monitor analytics performance metrics.
import { metricsCollector } from 'user-analytics-tracker';
// Metrics are automatically collected when enableMetrics is true
// Access metrics
const metrics = metricsCollector.getMetrics();
// Reset metrics
metricsCollector.reset();Location Consent Management
import {
setLocationConsentGranted,
hasLocationConsent,
checkAndSetLocationConsent,
clearLocationConsent,
} from 'user-analytics-tracker';
// When user enters MSISDN, grant location consent
checkAndSetLocationConsent(msisdn); // Returns true if consent granted
// Check if consent exists
if (hasLocationConsent()) {
// Location tracking allowed
}
// Manually grant/revoke consent
setLocationConsentGranted();
clearLocationConsent();Session Management
Enhanced session tracking utilities.
import {
getOrCreateSession,
updateSessionActivity,
getSession,
clearSession,
} from 'user-analytics-tracker';
// Get or create session with custom timeout (30 minutes)
const session = getOrCreateSession(30 * 60 * 1000);
// Returns: { sessionId, startTime, lastActivity, pageViews }
// Update session activity
updateSessionActivity();
// Get current session
const currentSession = getSession();
// Clear session
clearSession();Debug Utilities
Development debugging tools.
import { initDebug } from 'user-analytics-tracker';
// Initialize debug tools (automatically called in development)
initDebug();
// Then access via window.__analyticsDebug in browser consoleIP Geolocation Utilities
Client-Side: Get Public IP
import { getPublicIP } from 'user-analytics-tracker';
// Get just the public IP address (no location data)
const ip = await getPublicIP();
console.log('Your IP:', ip); // "203.0.113.42"Server-Side: IP Location from Request
import { getIPLocation, getIPFromRequest } from 'user-analytics-tracker';
// In your API route (Next.js example)
export async function POST(req: Request) {
const ip = getIPFromRequest(req);
// Free tier (ipwho.is)
const location = await getIPLocation(ip);
// Paid (ipwhois.pro): baseUrl/{IP}?key=API_KEY
// const location = await getIPLocation(ip, {
// baseUrl: 'https://ipwhois.pro',
// apiKey: process.env.IPWHOIS_PRO_API_KEY,
// });
// location contains: country, region, city, lat, lon, timezone, isp, etc.
}🔒 Privacy & Consent
MSISDN-Based Consent
When a user enters their phone number (MSISDN), it implies consent for location tracking. The library automatically grants location consent:
import { checkAndSetLocationConsent } from 'user-analytics-tracker';
// When MSISDN is entered
checkAndSetLocationConsent(phoneNumber);
// Location consent is now granted, GPS will be requested automaticallyHotspot Detection & Gating
Detect and restrict hotspot users:
import { useAnalytics } from 'user-analytics-tracker';
// Note: networkInfo is no longer available in essential mode
// Connection data is available in customData.ipLocation.connection from ipwho.is
function ConnectionInfo({ children }) {
// Connection info comes from ipwho.is API in analytics events
// Access via: customData.ipLocation.connection (asn, org, isp, domain)
return children;
}📖 Advanced Usage
Custom Analytics Service
import { AnalyticsService } from 'user-analytics-tracker';
class MyAnalyticsService extends AnalyticsService {
static async trackUserJourney(data: any) {
// Custom tracking logic
await fetch('/my-custom-endpoint', {
method: 'POST',
body: JSON.stringify(data),
});
}
}Custom Event Tracking (Firebase/GA-style)
Track custom events with automatic context collection:
const { trackEvent, trackPageView } = useAnalytics();
// Track button click
await trackEvent('button_click', {
button_name: 'signup',
button_location: 'header',
button_color: 'blue'
});
// Track purchase
await trackEvent('purchase', {
transaction_id: 'T12345',
value: 29.99,
currency: 'USD',
items: [
{ id: 'item1', name: 'Product 1', price: 29.99 }
]
});
// Track page views
await trackPageView('/dashboard', {
page_title: 'Dashboard',
user_type: 'premium'
});
// Track current page view
await trackPageView();Manual Event Tracking (Legacy)
const { logEvent, incrementInteraction } = useAnalytics();
// Log custom event with full control
await logEvent({
eventType: 'purchase',
productId: '123',
amount: 99.99,
});
// Increment interaction counter
incrementInteraction();Server-Side Integration
Example Next.js API route:
// app/api/analytics/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getIPFromRequest, getIPLocation } from 'user-analytics-tracker';
export async function POST(req: NextRequest) {
const body = await req.json();
const ip = getIPFromRequest(req);
const ipLocation = await getIPLocation(ip);
// Store analytics with IP location
await storeAnalytics({
...body,
ip,
ipLocation,
});
return NextResponse.json({ ok: true });
}📚 Documentation
Comprehensive documentation is available in the docs/ directory:
Upgrade Guide - Step-by-step migration instructions for upgrading between versions
Upgrade Guide - Step-by-step migration instructions for upgrading between versions
- Breaking changes and compatibility notes
- New features and improvements
- Migration examples
- Troubleshooting upgrade issues
Usage Guide - Complete guide on how to use the package in your applications
- Installation instructions
- Basic and advanced usage examples
- React hook documentation
- Standalone (non-React) usage
- Framework integrations (Next.js, Gatsby, etc.)
- Real-world examples
- Troubleshooting
Quick Start Guide - Get started in 5 minutes
- Installation
- Basic setup
- Development workflow
- Common commands
Publishing Guide - How to publish the package
- Prerequisites
- Publishing methods (automatic & manual)
- Version management
- Best practices
Package Structure - Understanding the codebase
- Directory structure
- Architecture overview
- Code organization
- Development guidelines
🧪 Testing
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage🛠️ Development
# Clone repository
git clone https://github.com/switch-org/analytics-tracker.git
cd analytics-tracker
# Install dependencies
npm install
# Build
npm run build
# Watch mode
npm run build:watch
# Lint
npm run lint
# Format
npm run format
# Type check
npm run type-check📝 TypeScript
This package is written in TypeScript and provides full type definitions. All exports are fully typed:
import type {
NetworkInfo,
DeviceInfo,
LocationInfo,
AttributionInfo,
IPLocation,
UseAnalyticsReturn,
} from 'user-analytics-tracker';🤝 Contributing
Contributions are welcome! Please read our contributing guidelines first.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Commit Convention
We follow Conventional Commits:
feat:New featurefix:Bug fixdocs:Documentation changesstyle:Code style changes (formatting, etc.)refactor:Code refactoringtest:Adding or updating testschore:Maintenance tasks
📄 License
MIT © Switch Org
🙏 Acknowledgments
- Uses ipwho.is for IP geolocation (free/paid). For paid users in the browser, use
config.ipGeolocation.proxyUrlso the API key is never exposed; see Field Storage / IP Geolocation. - Built with modern web APIs (User-Agent Client Hints, Geolocation API)
Made with ❤️ by ATIF RAFIQUE
