diff --git a/FEATURE_SHUTDOWN_EARLY_WARNING.md b/FEATURE_SHUTDOWN_EARLY_WARNING.md new file mode 100644 index 0000000..38231f3 --- /dev/null +++ b/FEATURE_SHUTDOWN_EARLY_WARNING.md @@ -0,0 +1,346 @@ +# Internet Shutdown Early Warning System + +## Overview + +The Internet Shutdown Early Warning System is an advanced anomaly detection feature that monitors user signup patterns to identify potential internet shutdowns or spam attacks. This feature is critical because **RelaySMS is a preparedness tool** - people sign up when they anticipate connectivity issues. + +## Key Concept + +Unlike traditional analytics where increased signups indicate growth, in RelaySMS: +- **Sudden signup spikes** = Potential impending internet shutdown +- **High retention after spike** = Legitimate shutdown preparation +- **Low retention after spike** = Possible spam/bot attack + +## Features + +### 1. Signup Spike Detection +- Monitors signups by country and date +- Compares current period to 7-day baseline +- Triggers alerts when signups increase by **200%+ or 100%+ with 50+ new signups** +- Considers both percentage change and absolute numbers + +### 2. Retention Validation +Distinguishes between real concerns and spam by analyzing user retention: + +``` +High Retention (≥50%) = Legitimate users preparing for shutdown +Low Retention (<50%) = Possible spam/bot attack +``` + +### 3. Alert Levels + +#### Critical (Red) - Spam Detection +- **Criteria**: Signup spike + retention rate <50% +- **Meaning**: Possible spam or bot attack +- **Action**: Investigate accounts, increase signup verification +- **Confidence**: ~75% + +#### High Risk (Orange) - Likely Shutdown +- **Criteria**: Signup spike + retention rate ≥70% +- **Meaning**: Legitimate shutdown preparation +- **Action**: Monitor closely, prepare infrastructure +- **Confidence**: 80-95% + +#### Medium Risk (Blue) - Monitor +- **Criteria**: Signup spike + retention rate 50-69% +- **Meaning**: Moderate activity, unclear intent +- **Action**: Watch for pattern confirmation +- **Confidence**: 50-80% + +#### Low Risk (Green) - Minor Spike +- **Criteria**: Signup spike + retention rate 0-49% but above spam threshold +- **Meaning**: Possible organic growth or minor concern +- **Action**: Continue monitoring +- **Confidence**: 30-50% + +### 4. Summary Metrics Dashboard + +Four key metric cards show at a glance: +- **Critical Alerts**: Possible spam attacks requiring immediate investigation +- **High Risk Alerts**: Likely shutdown preparation events +- **Medium Risk Alerts**: Situations requiring close monitoring +- **Total Countries Monitored**: Countries with active signup data + +### 5. Two View Modes + +#### Alert Dashboard View +- Shows all active alerts with detailed information +- Color-coded severity levels +- Key metrics for each country: + - Current signup count + - Baseline (7-day average) + - Percentage change + - Retention rate + - Confidence score + +#### Timeline View +- Visual line charts for top 10 countries +- Shows signup trends over the selected period +- Helps identify patterns and trends +- Useful for comparing historical data + +## How It Works + +### Detection Algorithm + +```javascript +1. Fetch baseline data (7 days before current period) +2. Fetch current period data +3. Fetch retention data +4. For each country: + a. Calculate current total signups + b. Calculate baseline average + c. Calculate percentage change + d. If spike detected (>200% or >100% with 50+ new signups): + - Calculate retention rate + - Classify alert level based on retention + - Assign confidence score + - Generate alert message +5. Sort alerts by severity and percentage change +6. Display in dashboard +``` + +### Retention Rate Calculation + +```javascript +Retention Rate = (Retained Users / Total Signups) × 100 + +Example: +- Country X had 300 signups in current period +- 210 of those users were retained (active) +- Retention Rate = (210 / 300) × 100 = 70% +- Classification: High Risk (likely shutdown prep) +``` + +### Confidence Score + +The confidence score is calculated based on: +- Retention rate percentage +- Magnitude of signup spike +- Historical patterns + +```javascript +For High Retention (≥50%): + confidence = 50 + (retention_rate / 2) + Range: 75-95% + +For Low Retention (<50%): + confidence = 30 + retention_rate + Range: 30-80% + +For Spam Detection: + confidence = fixed 75% +``` + +## Use Cases + +### 1. Shutdown Preparation +**Scenario**: Cameroon shows 340% signup spike with 72% retention + +**Interpretation**: +- High confidence this is legitimate shutdown preparation +- Users are actually using the service +- Likely an impending or ongoing internet shutdown + +**Actions**: +- Alert operations team to prepare infrastructure +- Monitor publication volume shifts (internet → SMS) +- Document the event for advocacy groups +- Consider reaching out to affected users + +### 2. Spam Detection +**Scenario**: Ethiopia shows 250% signup spike with 15% retention + +**Interpretation**: +- Users signing up but not using the service +- Likely bot attack or coordinated fake signups +- Low confidence in legitimate use + +**Actions**: +- Flag accounts for review +- Check for patterns (same IP, device, timing) +- Temporarily increase signup verification requirements +- Clean up inactive accounts + +### 3. Regional Monitoring +**Scenario**: Multiple countries in West Africa showing moderate spikes + +**Interpretation**: +- Could indicate regional political instability +- Users preparing for possible shutdowns +- Pattern suggests real concerns + +**Actions**: +- Monitor news and social media +- Prepare for increased load +- Coordinate with advocacy organizations +- Document patterns for reports + +## Data Sources + +### API Endpoints Used + +1. **Signup Data** + - Endpoint: `/signup` + - Parameters: `category=signup`, `group_by=country`, `granularity=day` + - Used for: Baseline and current period comparison + +2. **Retention Data** + - Endpoint: `/retained` + - Parameters: `category=retained`, `group_by=country`, `granularity=day` + - Used for: Validation of user legitimacy + +### Date Ranges + +- **Baseline Period**: 7 days before the selected start date +- **Current Period**: User-selected date range (default: last 30 days) +- **Comparison**: Current period compared to baseline to detect anomalies + +## Integration + +### Dashboard Integration + +The component is added to the main dashboard page: + +```jsx +import ShutdownEarlyWarning from 'sections/dashboard/default/ShutdownEarlyWarning'; + +// In dashboard render + + + +``` + +### Filter Support + +The component respects dashboard filters: +- **Date Range**: Changes baseline and current periods +- **Country Filter**: Focuses on specific country +- Other filters don't apply to signup/retention data + +## Interpreting Results + +### Example Alert Breakdown + +``` +🇨🇲 Cameroon (CM) - HIGH RISK ALERT +───────────────────────────────────── +Message: Possible shutdown preparation - Users actively using service +Confidence: 86% + +Metrics: +├─ Current Signups: 1,230 ↑ +├─ Baseline (7-day): 285 +├─ Change: +331.6% +└─ Retention: 72% ✓ + +Interpretation: +This is likely a legitimate response to anticipated internet restrictions. +The high retention rate (72%) indicates users are genuinely concerned +and actively using the service. +``` + +### Reading Timeline Charts + +- **Flat line**: Normal, stable activity +- **Gradual increase**: Organic growth +- **Sharp spike**: Anomaly requiring investigation +- **Drop after spike**: May indicate shutdown ended or spam cleaned up + +## Best Practices + +### 1. Regular Monitoring +- Check dashboard daily during high-risk periods +- Set up alerts for High Risk and Critical levels +- Review trends weekly to identify patterns + +### 2. Context Awareness +- Cross-reference with news and social media +- Consider regional political situations +- Look for patterns across neighboring countries + +### 3. Response Planning +- Document all detected events +- Maintain contact list for advocacy groups +- Have infrastructure scaling plan ready +- Prepare user communication templates + +### 4. False Positive Management +- Investigate all Critical alerts promptly +- Verify High Risk alerts with external sources +- Adjust thresholds based on historical data +- Document false positives for algorithm improvement + +## Limitations + +1. **Requires Historical Data**: Needs at least 7 days of baseline data +2. **Retention Lag**: Retention data may lag by days +3. **Cannot Predict**: Only detects preparation, not actual shutdowns +4. **Regional Variations**: Thresholds may need adjustment per region +5. **External Factors**: Legitimate growth can trigger false positives + +## Future Enhancements + +### Planned Improvements +1. Machine learning model trained on historical shutdown data +2. Integration with external shutdown tracking APIs +3. Automated notifications via email/webhook +4. SMS vs. Internet publication routing analysis +5. Historical shutdown archive +6. Multi-factor confidence scoring +7. Real-time monitoring mode +8. Export reports for advocacy use + +### Suggested Thresholds Adjustment +- Consider country-specific baselines +- Adjust spike threshold based on country size +- Weight recent history more than older data +- Add time-of-day patterns + +## Technical Details + +### Component Location +`src/sections/dashboard/default/ShutdownEarlyWarning.jsx` + +### Key Functions + +1. **fetchSignupData**: Retrieves signup data from API +2. **fetchRetainedData**: Retrieves retention data from API +3. **calculateRetentionRate**: Computes retention percentage per country +4. **detectAnomalies**: Main detection algorithm +5. **processTimelineData**: Prepares data for visualization + +### Dependencies +- Material-UI components +- @mui/x-charts for LineChart +- axios for API calls +- dayjs for date manipulation + +### State Management +- `alerts`: Array of detected anomalies +- `countryTimelines`: Timeline data for visualization +- `loading`: Loading state +- `view`: Current view mode (alerts/timeline) + +## Support + +For questions or issues: +1. Check documentation in the app (Help section) +2. Review this technical guide +3. Contact development team +4. Submit issue on GitHub + +## Changelog + +### Version 1.0.0 (December 2025) +- Initial release +- Signup spike detection +- Retention validation +- Alert classification system +- Dual view mode (alerts/timelines) +- Summary metrics dashboard +- Integration with dashboard filters + +--- + +**Note**: This feature is critical for RelaySMS operations. Regular monitoring and prompt investigation of alerts can help identify both security threats (spam) and human rights concerns (shutdowns). diff --git a/src/pages/dashboard/default.jsx b/src/pages/dashboard/default.jsx index 68f6b8b..27ba77c 100644 --- a/src/pages/dashboard/default.jsx +++ b/src/pages/dashboard/default.jsx @@ -5,7 +5,7 @@ import Box from '@mui/material/Box'; import { useTheme } from '@mui/material/styles'; // ant design -import { DatePicker, Select, Button, Dropdown, Switch } from 'antd'; +import { DatePicker, Select, Button, Dropdown, Switch, Radio } from 'antd'; import 'antd/dist/reset.css'; // project imports @@ -16,6 +16,7 @@ import ErrorDisplay from 'components/ErrorDisplay'; import CombinedChartCard from 'sections/dashboard/default/CombinedChartCard'; import UserTable from 'sections/dashboard/default/UserTable'; import UserRetentionMetrics from 'sections/dashboard/default/UserRetentionMetrics'; +import ShutdownEarlyWarning from 'sections/dashboard/default/ShutdownEarlyWarning'; import axios from 'axios'; import { useEffect, useState } from 'react'; @@ -66,12 +67,15 @@ export default function DashboardDefault() { const [granularity, setGranularity] = useState('day'); const [groupBy, setGroupBy] = useState('date'); const [countryCode, setCountryCode] = useState(null); + const [type, setType] = useState(null); + const [origin, setOrigin] = useState(null); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [dateRangeFilter, setDateRangeFilter] = useState('custom'); const [showCustomDatePickers, setShowCustomDatePickers] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); const [loading, setLoading] = useState(true); + const [downloading, setDownloading] = useState(false); const [error, setError] = useState(null); const [filtersApplied, setFiltersApplied] = useState({}); const [availableCountries, setAvailableCountries] = useState([]); @@ -242,7 +246,7 @@ export default function DashboardDefault() { if (startDate && endDate) { return `${startDate.format('YYYY-MM-DD')} - ${endDate.format('YYYY-MM-DD')}`; } - return '2021-01-10 - Today'; + return '2020-01-10 - Today'; default: return 'Date Range Filter'; } @@ -252,24 +256,28 @@ export default function DashboardDefault() { const today = new Date().toISOString().split('T')[0]; const appliedFilters = { category, - startDate: startDate ? startDate.format('YYYY-MM-DD') : '2021-01-10', + startDate: startDate ? startDate.format('YYYY-MM-DD') : '2020-01-10', endDate: endDate ? endDate.format('YYYY-MM-DD') : today, granularity, // groupBy, - countryCode: countryCode || undefined + countryCode: countryCode || undefined, + type: type || undefined, + origin: origin || undefined }; setFiltersApplied(appliedFilters); }; const handleResetFilters = () => { - const resetStartDate = dayjs('2021-01-01'); + const resetStartDate = dayjs('2020-01-01'); const resetEndDate = dayjs(); setCategory('all'); // setGranularity('month'); // setGroupBy('country'); setCountryCode(null); + setType(null); + setOrigin(null); setStartDate(resetStartDate); setEndDate(resetEndDate); setDateRangeFilter('custom'); @@ -285,48 +293,152 @@ export default function DashboardDefault() { }; const handleDownloadData = async () => { + setDownloading(true); try { const today = new Date().toISOString().split('T')[0]; - const appliedStart = filtersApplied.startDate || '2021-01-10'; + const appliedStart = filtersApplied.startDate || '2020-01-10'; const appliedEnd = filtersApplied.endDate || today; + const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API; + + const countryParam = filtersApplied.countryCode ? `&country_code=${filtersApplied.countryCode}` : ''; + const typeParam = filtersApplied.type ? `&type=${filtersApplied.type}` : ''; + const originParam = filtersApplied.origin ? `&origin=${filtersApplied.origin}` : ''; + const currentCategory = filtersApplied.category || category; + + const summaryUrl = `${baseUrl}summary?start_date=${appliedStart}&end_date=${appliedEnd}${countryParam}${typeParam}${originParam}`; + const summaryResponse = await axios.get(summaryUrl); + const summaryData = summaryResponse.data.summary; + + const fetchAllPages = async (endpoint, category, groupBy = 'date') => { + const params = { + category, + start_date: appliedStart, + end_date: appliedEnd, + granularity: 'day', + group_by: groupBy, + page: 1, + page_size: 100 + }; - let apiUrl = `${import.meta.env.VITE_APP_TELEMETRY_API}summary?start_date=${appliedStart}&end_date=${appliedEnd}`; + if (filtersApplied.countryCode) { + params.country_code = filtersApplied.countryCode; + } + + if (filtersApplied.type) { + params.type = filtersApplied.type; + } + + if (filtersApplied.origin) { + params.origin = filtersApplied.origin; + } + + const firstResponse = await axios.get(`${baseUrl}${endpoint}`, { params }); + const totalPages = firstResponse?.data?.[category]?.pagination?.total_pages || 1; + let allData = firstResponse?.data?.[category]?.data ?? []; + + if (totalPages > 1) { + const pagePromises = []; + for (let page = 2; page <= totalPages; page++) { + pagePromises.push(axios.get(`${baseUrl}${endpoint}`, { params: { ...params, page } })); + } + + const responses = await Promise.all(pagePromises); + responses.forEach((response) => { + const pageData = response?.data?.[category]?.data ?? []; + allData = [...allData, ...pageData]; + }); + } + return allData; + }; + + const signupByDate = await fetchAllPages('signup', 'signup', 'date'); + const signupByCountry = await fetchAllPages('signup', 'signup', 'country'); + + const retainedByDate = await fetchAllPages('retained', 'retained', 'date'); + const retainedByCountry = await fetchAllPages('retained', 'retained', 'country'); + + const pubParams = { + start_date: appliedStart, + end_date: appliedEnd, + page: 1, + page_size: 100 + }; if (filtersApplied.countryCode) { - apiUrl += `&country_code=${filtersApplied.countryCode}`; + pubParams.country_code = filtersApplied.countryCode; + } + if (filtersApplied.type) { + pubParams.type = filtersApplied.type; } + if (filtersApplied.origin) { + pubParams.origin = filtersApplied.origin; + } + + const firstPubResponse = await axios.get(`${baseUrl}publications`, { params: pubParams }); + const totalPubPages = firstPubResponse?.data?.publications?.pagination?.total_pages || 1; + let allPublications = firstPubResponse?.data?.publications?.data ?? []; - const response = await axios.get(apiUrl); - const data = response.data.summary; + if (totalPubPages > 1) { + const pubPagePromises = []; + for (let page = 2; page <= totalPubPages; page++) { + pubPagePromises.push(axios.get(`${baseUrl}publications`, { params: { ...pubParams, page } })); + } + + const pubResponses = await Promise.all(pubPagePromises); + pubResponses.forEach((response) => { + const pageData = response?.data?.publications?.data ?? []; + allPublications = [...allPublications, ...pageData]; + }); + } const downloadData = { metadata: { exportDate: new Date().toISOString(), filters: { - category: filtersApplied.category || category, + category: currentCategory, startDate: appliedStart, endDate: appliedEnd, - countryCode: filtersApplied.countryCode || countryCode || 'All Countries' + countryCode: filtersApplied.countryCode || 'All Countries', + type: filtersApplied.type || 'All Types', + origin: filtersApplied.origin || 'All Origins' + }, + recordCounts: { + signupByDate: signupByDate.length, + signupByCountry: signupByCountry.length, + retainedByDate: retainedByDate.length, + retainedByCountry: retainedByCountry.length, + publications: allPublications.length } }, summary: { signup: { - total_signup_users: data.total_signup_users || 0, - total_signups_from_bridges: data.total_signups_from_bridges || 0, - signup_countries: data.signup_countries || [], - total_signup_countries: data.total_signup_countries || 0 + total_signup_users: summaryData.total_signup_users || 0, + total_signups_from_bridges: summaryData.total_signups_from_bridges || 0, + signup_countries: summaryData.signup_countries || [], + total_signup_countries: summaryData.total_signup_countries || 0 }, retained: { - total_retained_users: data.total_retained_users || 0, - total_retained_users_with_tokens: data.total_retained_users_with_tokens || 0, - retained_countries: data.retained_countries || [], - total_retained_countries: data.total_retained_countries || 0 + total_retained_users: summaryData.total_retained_users || 0, + total_retained_users_with_tokens: summaryData.total_retained_users_with_tokens || 0, + retained_countries: summaryData.retained_countries || [], + total_retained_countries: summaryData.total_retained_countries || 0 }, publications: { - total_publications: data.total_publications || 0 + total_publications: summaryData.total_publications || 0 } }, - rawData: data + detailedData: { + signup: { + byDate: signupByDate, + byCountry: signupByCountry + }, + retained: { + byDate: retainedByDate, + byCountry: retainedByCountry + }, + publications: allPublications + }, + rawSummary: summaryData }; const jsonString = JSON.stringify(downloadData, null, 2); @@ -337,7 +449,7 @@ export default function DashboardDefault() { link.href = url; const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); - link.download = `telemetry-dashboard-data-${timestamp}.json`; + link.download = `telemetry-dashboard-complete-${timestamp}.json`; document.body.appendChild(link); link.click(); @@ -347,6 +459,8 @@ export default function DashboardDefault() { } catch (error) { console.error('Error downloading data:', error); alert('Failed to download data. Please try again.'); + } finally { + setDownloading(false); } }; @@ -355,7 +469,7 @@ export default function DashboardDefault() { setLoading(true); try { const today = new Date().toISOString().split('T')[0]; - const appliedStart = filtersApplied.startDate || '2021-01-10'; + const appliedStart = filtersApplied.startDate || '2020-01-10'; const appliedEnd = filtersApplied.endDate || today; const startDateObj = dayjs(appliedStart); @@ -373,6 +487,16 @@ export default function DashboardDefault() { previousApiUrl += `&country_code=${filtersApplied.countryCode}`; } + if (filtersApplied.type) { + currentApiUrl += `&type=${filtersApplied.type}`; + previousApiUrl += `&type=${filtersApplied.type}`; + } + + if (filtersApplied.origin) { + currentApiUrl += `&origin=${filtersApplied.origin}`; + previousApiUrl += `&origin=${filtersApplied.origin}`; + } + const [currentResponse, previousResponse] = await Promise.all([axios.get(currentApiUrl), axios.get(previousApiUrl)]); const data = currentResponse.data.summary; @@ -666,6 +790,30 @@ export default function DashboardDefault() { /> + {/* Type Filter */} + + + Type + + setType(e.target.value)} style={{ width: '100%' }}> + All + Phone + Email + + + + {/* Origin Filter */} + + + Origin + + setOrigin(e.target.value)} style={{ width: '100%' }}> + All + Web + Bridge + + + {/* Buttons */} @@ -679,8 +827,15 @@ export default function DashboardDefault() { - @@ -719,6 +874,11 @@ export default function DashboardDefault() { {/* */} + + {/* row 5: Internet Shutdown Early Warning */} + {/* + + */} ); } diff --git a/src/pages/extra-pages/documentation.jsx b/src/pages/extra-pages/documentation.jsx index b16e1c5..1ae90f1 100644 --- a/src/pages/extra-pages/documentation.jsx +++ b/src/pages/extra-pages/documentation.jsx @@ -422,6 +422,56 @@ export default function Documentation() { secondary="Interactive visualization showing user distribution across countries. Click on countries to see detailed statistics." /> + + Internet Shutdown Early Warning System:} + secondary={ + + Advanced anomaly detection that monitors signup patterns to identify potential internet shutdowns or spam attacks. + This is a critical feature because RelaySMS is a preparedness tool - people sign up when they anticipate connectivity + issues: +
    +
  • + Signup Spike Detection - Automatically detects when signups from a country increase by 200%+ + above the 7-day baseline +
  • +
  • + Retention Validation - Distinguishes real shutdown preparation (high retention) from spam attacks + (low retention) +
  • +
  • + Alert Levels: +
      +
    • + Critical (Red) - Low retention (<50%) indicates possible spam + or bot attack. Requires investigation. +
    • +
    • + High Risk (Orange) - High retention (≥70%) with major spike. + Likely legitimate shutdown preparation. +
    • +
    • + Medium Risk (Blue) - Moderate retention (50-70%) with spike. + Monitor situation closely. +
    • +
    +
  • +
  • + Confidence Score - Percentage indicating how certain the system is about the classification +
  • +
  • + Timeline View - Visual charts showing signup trends by country over time to spot patterns +
  • +
+ Understanding the Logic: When people expect internet shutdowns, they prepare by signing up for + RelaySMS to maintain communication via SMS. A sudden spike in signups from a specific country, combined with high user + retention (users actually using the service), strongly suggests an impending or ongoing shutdown. Conversely, a spike + with low retention suggests fake accounts or spam. Use this tool to monitor high-risk regions, prepare infrastructure + for increased load, and potentially alert advocacy groups about shutdowns. +
+ } + /> +
{ const appliedFilters = { platform, - startDate: startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2021-01-10', + startDate: startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2020-01-10', endDate: endDate ? dayjs(endDate).format('YYYY-MM-DD') : today, status, source, @@ -319,7 +320,7 @@ export default function Publications() { if (startDate && endDate) { return `${startDate.format('YYYY-MM-DD')} - ${endDate.format('YYYY-MM-DD')}`; } - return '2021-01-10 - Today'; + return '2020-01-10 - Today'; default: return 'Date Range Filter'; } @@ -338,9 +339,10 @@ export default function Publications() { }; const handleDownloadData = async () => { + setDownloading(true); try { const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API; - const appliedStart = filtersApplied.startDate || '2021-01-10'; + const appliedStart = filtersApplied.startDate || '2020-01-10'; const appliedEnd = filtersApplied.endDate || today; const params = { @@ -406,6 +408,8 @@ export default function Publications() { } catch (error) { console.error('Error downloading publications data:', error); alert('Failed to download data. Please try again.'); + } finally { + setDownloading(false); } }; @@ -463,7 +467,7 @@ export default function Publications() { const fetchMetrics = async () => { setLoading(true); try { - const appliedStart = startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2021-01-10'; + const appliedStart = startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2020-01-10'; const appliedEnd = endDate ? dayjs(endDate).format('YYYY-MM-DD') : today; const startDateObj = dayjs(appliedStart); @@ -600,7 +604,7 @@ export default function Publications() { const fetchAllCountries = async () => { try { const params = { - start_date: '2021-01-10', + start_date: '2020-01-10', end_date: new Date().toISOString().split('T')[0], page: 1, page_size: 100 @@ -641,8 +645,8 @@ export default function Publications() { Publications - diff --git a/src/pages/reliability/reliabilityTable.jsx b/src/pages/reliability/reliabilityTable.jsx index d9dd2df..0532b53 100644 --- a/src/pages/reliability/reliabilityTable.jsx +++ b/src/pages/reliability/reliabilityTable.jsx @@ -134,7 +134,7 @@ export default function ReliabilityTable() { operator, operatorCode, reliability, - dateFilter: dateFilter ? dayjs(dateFilter).format('YYYY-MM-DD') : '2021-01-10' + dateFilter: dateFilter ? dayjs(dateFilter).format('YYYY-MM-DD') : '2020-01-10' }; setFiltersApplied(appliedFilters); setPage(0); @@ -153,7 +153,7 @@ export default function ReliabilityTable() { const handleDownloadData = async () => { try { - const appliedDate = filtersApplied.dateFilter || '2021-01-10'; + const appliedDate = filtersApplied.dateFilter || '2020-01-10'; const params = { date_filter: appliedDate, @@ -261,7 +261,7 @@ export default function ReliabilityTable() { setLoading(true); setError(null); try { - const appliedDate = filtersApplied.dateFilter || '2021-01-10'; + const appliedDate = filtersApplied.dateFilter || '2020-01-10'; const params = { date_filter: appliedDate, diff --git a/src/sections/dashboard/UserBarChart.jsx b/src/sections/dashboard/UserBarChart.jsx index 2d1b602..c5d3dc0 100644 --- a/src/sections/dashboard/UserBarChart.jsx +++ b/src/sections/dashboard/UserBarChart.jsx @@ -46,13 +46,15 @@ export default function UserBarChart({ view, filters }) { try { const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API; const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : ''; + const typeParam = filters?.type ? `&type=${filters.type}` : ''; + const originParam = filters?.origin ? `&origin=${filters.origin}` : ''; const signupResponse = await axios.get( - `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}` + `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}` ); const retainedResponse = await axios.get( - `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}` + `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}` ); const signup = signupResponse?.data?.signup?.data ?? []; @@ -78,7 +80,7 @@ export default function UserBarChart({ view, filters }) { }; fetchData(); - }, [startDate, endDate, granularity, currentPage, filters?.countryCode]); + }, [startDate, endDate, granularity, currentPage, filters?.countryCode, filters?.type, filters?.origin]); const axisFontStyle = { fontSize: 10, fill: theme.palette.text.secondary }; @@ -100,7 +102,7 @@ export default function UserBarChart({ view, filters }) { User Data - Signups & Retained + Signups & Current @@ -123,7 +125,7 @@ export default function UserBarChart({ view, filters }) { sx={{ '&.Mui-checked': { color: secondaryColor } }} /> } - label="Retained" + label="Current" /> @@ -136,7 +138,7 @@ export default function UserBarChart({ view, filters }) { yAxis={[{ disableLine: true, disableTicks: true, tickLabelStyle: axisFontStyle }]} series={[ ...(showSignups ? [{ data: signupData, label: 'Signups', color: primaryColor, type: 'bar' }] : []), - ...(showRetained ? [{ data: retainedData, label: 'Retained', color: secondaryColor, type: 'bar' }] : []) + ...(showRetained ? [{ data: retainedData, label: 'Current', color: secondaryColor, type: 'bar' }] : []) ]} slotProps={{ legend: { hidden: true }, bar: { rx: 5, ry: 5 } }} axisHighlight={{ x: 'none' }} diff --git a/src/sections/dashboard/default/CountryTable.jsx b/src/sections/dashboard/default/CountryTable.jsx index 0846d6f..3aca74b 100644 --- a/src/sections/dashboard/default/CountryTable.jsx +++ b/src/sections/dashboard/default/CountryTable.jsx @@ -65,13 +65,15 @@ export default function CountryTable({ filters, onCountryClick, selectedCountry useEffect(() => { setPage(0); - }, [effectiveStartDate, effectiveEndDate, effectiveCategory, effectiveGranularity, filters?.countryCode]); + }, [effectiveStartDate, effectiveEndDate, effectiveCategory, effectiveGranularity, filters?.countryCode, filters?.type, filters?.origin]); useEffect(() => { const fetchCountryData = async () => { setLoading(true); try { const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : ''; + const typeParam = filters?.type ? `&type=${filters.type}` : ''; + const originParam = filters?.origin ? `&origin=${filters.origin}` : ''; if (effectiveCategory === 'all') { const fetchAllPages = async (endpoint) => { @@ -81,7 +83,7 @@ export default function CountryTable({ filters, onCountryClick, selectedCountry do { const response = await axios.get( - `${import.meta.env.VITE_APP_TELEMETRY_API}${endpoint}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${currentPage}&page_size=25${countryParam}` + `${import.meta.env.VITE_APP_TELEMETRY_API}${endpoint}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${currentPage}&page_size=25${countryParam}${typeParam}${originParam}` ); const categoryKey = endpoint.includes('retained') ? 'retained' : 'signup'; @@ -152,7 +154,7 @@ export default function CountryTable({ filters, onCountryClick, selectedCountry setCountryData(paginatedData); } else { const response = await axios.get( - `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${page + 1}&page_size=${rowsPerPage}${countryParam}` + `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}` ); const categoryKey = effectiveCategory.includes('retained') ? 'retained' : 'signup'; diff --git a/src/sections/dashboard/default/Map.jsx b/src/sections/dashboard/default/Map.jsx index 7562133..246429d 100644 --- a/src/sections/dashboard/default/Map.jsx +++ b/src/sections/dashboard/default/Map.jsx @@ -42,14 +42,16 @@ export default function CountryMap({ filters, selectedCountry, onCountrySelect } setLoading(true); try { const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : ''; + const typeParam = filters?.type ? `&type=${filters.type}` : ''; + const originParam = filters?.origin ? `&origin=${filters.origin}` : ''; if (effectiveCategory === 'all') { const [signupResponse, retainedResponse] = await Promise.all([ axios.get( - `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}` + `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}` ), axios.get( - `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}` + `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}` ) ]); @@ -111,7 +113,7 @@ export default function CountryMap({ filters, selectedCountry, onCountrySelect } setCountryData(formatted); } else { const response = await axios.get( - `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}` + `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}` ); const categoryKey = effectiveCategory.includes('retained') ? 'retained' : 'signup'; @@ -153,7 +155,7 @@ export default function CountryMap({ filters, selectedCountry, onCountrySelect } } finally { setLoading(false); } - }, [effectiveCategory, effectiveStartDate, effectiveEndDate, effectiveGranularity, filters?.countryCode]); + }, [effectiveCategory, effectiveStartDate, effectiveEndDate, effectiveGranularity, filters?.countryCode, filters?.type, filters?.origin]); useEffect(() => { fetchCountryData(); diff --git a/src/sections/dashboard/default/ShutdownEarlyWarning.jsx b/src/sections/dashboard/default/ShutdownEarlyWarning.jsx new file mode 100644 index 0000000..d4a3f7d --- /dev/null +++ b/src/sections/dashboard/default/ShutdownEarlyWarning.jsx @@ -0,0 +1,517 @@ +import { useEffect, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { + Box, + Typography, + Grid, + Card, + CardContent, + Chip, + Alert, + AlertTitle, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + ToggleButtonGroup, + ToggleButton +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { LineChart } from '@mui/x-charts/LineChart'; +import { WarningOutlined } from '@ant-design/icons'; +import { CheckCircleOutlined } from '@ant-design/icons'; +import axios from 'axios'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { RiseOutlined } from '@ant-design/icons'; +import { FallOutlined } from '@ant-design/icons'; + +// components +import MainCard from 'components/MainCard'; +import Loader from 'components/Loader'; + +dayjs.extend(utc); + +export default function ShutdownEarlyWarning({ filters }) { + const theme = useTheme(); + const [loading, setLoading] = useState(true); + const [alerts, setAlerts] = useState([]); + const [countryTimelines, setCountryTimelines] = useState([]); + const [view, setView] = useState('alerts'); + + const today = new Date().toISOString().split('T')[0]; + const startDate = filters?.startDate || dayjs().subtract(30, 'day').format('YYYY-MM-DD'); + const endDate = filters?.endDate || today; + const countryFilter = filters?.countryCode || ''; + const typeFilter = filters?.type || ''; + const originFilter = filters?.origin || ''; + + const baselineStartDate = dayjs(startDate).subtract(7, 'day').format('YYYY-MM-DD'); + const baselineEndDate = dayjs(startDate).subtract(1, 'day').format('YYYY-MM-DD'); + + const fetchSignupData = useCallback(async (start, end, country = '', type = '', origin = '') => { + const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API; + const countryParam = country ? `&country_code=${country}` : ''; + const typeParam = type ? `&type=${type}` : ''; + const originParam = origin ? `&origin=${origin}` : ''; + + const response = await axios.get( + `${baseUrl}signup?category=signup&start_date=${start}&end_date=${end}&granularity=day&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}` + ); + + return response?.data?.signup?.data ?? []; + }, []); + + const fetchRetainedData = useCallback(async (start, end, country = '', type = '', origin = '') => { + const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API; + const countryParam = country ? `&country_code=${country}` : ''; + const typeParam = type ? `&type=${type}` : ''; + const originParam = origin ? `&origin=${origin}` : ''; + + const response = await axios.get( + `${baseUrl}retained?category=retained&start_date=${start}&end_date=${end}&granularity=day&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}` + ); + + return response?.data?.retained?.data ?? []; + }, []); + + const calculateRetentionRate = useCallback((signups, retained, country) => { + const countrySignups = signups.filter((s) => s.country_code === country); + const countryRetained = retained.filter((r) => r.country_code === country); + + const totalSignups = countrySignups.reduce((sum, s) => sum + (s.signup_users || 0), 0); + const totalRetained = countryRetained.reduce((sum, r) => sum + (r.retained_users || 0), 0); + + if (totalSignups === 0) return 0; + return ((totalRetained / totalSignups) * 100).toFixed(1); + }, []); + + const detectAnomalies = useCallback( + (baselineData, currentData, retainedData) => { + const anomalies = []; + + const currentByCountry = currentData.reduce((acc, item) => { + const country = item.country_code; + if (!acc[country]) { + acc[country] = { signups: 0, countryName: item.country_name }; + } + acc[country].signups += item.signup_users || 0; + return acc; + }, {}); + + const baselineByCountry = baselineData.reduce((acc, item) => { + const country = item.country_code; + if (!acc[country]) { + acc[country] = { signups: 0 }; + } + acc[country].signups += item.signup_users || 0; + return acc; + }, {}); + + Object.entries(currentByCountry).forEach(([country, data]) => { + const currentSignups = data.signups; + const baselineSignups = baselineByCountry[country]?.signups || 0; + + let percentageChange = 0; + if (baselineSignups > 0) { + percentageChange = ((currentSignups - baselineSignups) / baselineSignups) * 100; + } else if (currentSignups > 0) { + percentageChange = 100; + } + + if (percentageChange > 200 || (currentSignups - baselineSignups > 50 && percentageChange > 100)) { + const retentionRate = parseFloat(calculateRetentionRate(currentData, retainedData, country)); + + let alertType = 'info'; + let alertLevel = 'Low'; + let message = ''; + let confidence = 0; + + if (retentionRate >= 50) { + alertType = 'warning'; + alertLevel = retentionRate >= 70 ? 'High' : 'Medium'; + message = 'Possible shutdown preparation - Users actively using service'; + confidence = Math.min(95, 50 + retentionRate / 2); + } else if (retentionRate < 50 && retentionRate > 0) { + alertType = 'info'; + alertLevel = 'Low'; + message = 'Moderate retention - Monitor for confirmation'; + confidence = 30 + retentionRate; + } else { + alertType = 'error'; + alertLevel = 'Critical'; + message = 'Possible spam/bot attack - Low retention rate'; + confidence = 75; + } + + anomalies.push({ + country, + countryName: data.countryName || country, + currentSignups, + baselineSignups, + percentageChange: percentageChange.toFixed(1), + retentionRate, + alertType, + alertLevel, + message, + confidence: confidence.toFixed(0), + detectedAt: new Date().toISOString() + }); + } + }); + + return anomalies.sort((a, b) => { + const severityOrder = { High: 3, Medium: 2, Low: 1, Critical: 4 }; + if (severityOrder[a.alertLevel] !== severityOrder[b.alertLevel]) { + return severityOrder[b.alertLevel] - severityOrder[a.alertLevel]; + } + return b.percentageChange - a.percentageChange; + }); + }, + [calculateRetentionRate] + ); + + const processTimelineData = useCallback((data) => { + const grouped = data.reduce((acc, item) => { + const country = item.country_code; + const date = item.timeframe; + + if (!acc[country]) { + acc[country] = { + countryName: item.country_name || country, + data: [] + }; + } + + acc[country].data.push({ + date, + signups: item.signup_users || 0 + }); + + return acc; + }, {}); + + return Object.entries(grouped) + .map(([country, info]) => ({ + country, + countryName: info.countryName, + totalSignups: info.data.reduce((sum, d) => sum + d.signups, 0), + timeline: info.data.sort((a, b) => new Date(a.date) - new Date(b.date)) + })) + .sort((a, b) => b.totalSignups - a.totalSignups) + .slice(0, 10); + }, []); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + const baselineSignups = await fetchSignupData(baselineStartDate, baselineEndDate, countryFilter, typeFilter, originFilter); + + const currentSignups = await fetchSignupData(startDate, endDate, countryFilter, typeFilter, originFilter); + + const retainedUsers = await fetchRetainedData(startDate, endDate, countryFilter, typeFilter, originFilter); + + const detectedAnomalies = detectAnomalies(baselineSignups, currentSignups, retainedUsers); + setAlerts(detectedAnomalies); + + const allData = [...baselineSignups, ...currentSignups]; + const timelines = processTimelineData(allData); + setCountryTimelines(timelines); + } catch (err) { + console.error('Error fetching shutdown warning data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [ + startDate, + endDate, + countryFilter, + typeFilter, + originFilter, + baselineStartDate, + baselineEndDate, + fetchSignupData, + fetchRetainedData, + detectAnomalies, + processTimelineData + ]); + + const getAlertIcon = (alertType) => { + switch (alertType) { + case 'warning': + return ; + case 'error': + return ; + case 'info': + return ; + default: + return ; + } + }; + + const getAlertColor = (alertType) => { + switch (alertType) { + case 'warning': + return theme.palette.warning.main; + case 'error': + return theme.palette.error.main; + case 'info': + return theme.palette.info.main; + default: + return theme.palette.success.main; + } + }; + + const handleViewChange = (event, newView) => { + if (newView !== null) { + setView(newView); + } + }; + + if (loading) { + return ( + + + + + + ); + } + + return ( + + + + + + Internet Shutdown Early Warning System + + + Monitors signup spikes and retention patterns to detect potential shutdowns or spam attacks + + + + + Alert Dashboard + Country Timelines + + + + {/* Summary Metrics */} + + + + + Critical Alerts + + + {alerts.filter((a) => a.alertLevel === 'Critical').length} + + Possible spam attacks + + + + + + High Risk Alerts + + + {alerts.filter((a) => a.alertLevel === 'High').length} + + Likely shutdown prep + + + + + + Medium Risk Alerts + + + {alerts.filter((a) => a.alertLevel === 'Medium').length} + + Monitor closely + + + + + + Total Countries Monitored + + + {countryTimelines.length} + + Active countries + + + + + + {alerts.length === 0 && view === 'alerts' ? ( + + + + No Anomalies Detected + + + All signup patterns appear normal. The system is monitoring for unusual activity. + + + ) : view === 'alerts' ? ( + + + Active Alerts ({alerts.length}) + + + {alerts.map((alert, index) => ( + + + + + {alert.countryName} ({alert.country}) + + + + + + {alert.message} + + + + + Current Signups + + + {alert.currentSignups} + + + + + + Baseline (7-day avg) + + {alert.baselineSignups} + + + + Change + + + +{alert.percentageChange}% + + + + + Retention Rate + + = 50 ? theme.palette.success.main : theme.palette.error.main + }} + > + {alert.retentionRate}% + + + + + + ))} + + + ) : ( + // Timeline View + + + Signup Trends by Country (Top 10) + + {countryTimelines.length === 0 ? ( + + + No timeline data available for the selected period + + + ) : ( + + {countryTimelines.slice(0, 6).map((countryData, index) => ( + + + + {countryData.countryName} ({countryData.country}) + + + Total Signups: {countryData.totalSignups.toLocaleString()} + + + idx), + scaleType: 'point', + tickLabelStyle: { + fontSize: 10, + fill: theme.palette.text.primary + } + } + ]} + yAxis={[ + { + tickLabelStyle: { + fontSize: 10, + fill: theme.palette.text.primary + } + } + ]} + series={[ + { + data: countryData.timeline.map((t) => t.signups), + curve: 'monotoneX', + showMark: true, + color: theme.palette.primary.main + } + ]} + margin={{ top: 10, bottom: 30, left: 40, right: 10 }} + grid={{ horizontal: true }} + /> + + + + ))} + + )} + + )} + + ); +} + +ShutdownEarlyWarning.propTypes = { + filters: PropTypes.object +}; diff --git a/src/sections/dashboard/default/UserAreaChart.jsx b/src/sections/dashboard/default/UserAreaChart.jsx index 16eddea..182f79c 100644 --- a/src/sections/dashboard/default/UserAreaChart.jsx +++ b/src/sections/dashboard/default/UserAreaChart.jsx @@ -65,13 +65,15 @@ export default function UserAreaChart({ view, filters }) { try { const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API; const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : ''; + const typeParam = filters?.type ? `&type=${filters.type}` : ''; + const originParam = filters?.origin ? `&origin=${filters.origin}` : ''; const signupResponse = await axios.get( - `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}` + `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}` ); const retainedResponse = await axios.get( - `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}` + `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}` ); const signup = signupResponse?.data?.signup?.data ?? []; @@ -97,7 +99,7 @@ export default function UserAreaChart({ view, filters }) { }; fetchData(); - }, [startDate, endDate, granularity, currentPage, filters?.countryCode]); + }, [startDate, endDate, granularity, currentPage, filters?.countryCode, filters?.type, filters?.origin]); const toggleVisibility = (label) => { setVisibility((prev) => ({ ...prev, [label]: !prev[label] })); @@ -115,12 +117,12 @@ export default function UserAreaChart({ view, filters }) { }, { data: retainedData, - label: 'Retained', + label: 'Current', showMark: false, area: true, - id: 'Retained', + id: 'Current', color: theme.palette.primary[700], - visible: visibility['Retained'] + visible: visibility['Current'] } ]; @@ -161,7 +163,7 @@ export default function UserAreaChart({ view, filters }) { slotProps={{ legend: { hidden: true } }} sx={{ '& .MuiAreaElement-series-Signups': { fill: "url('#myGradient1')", strokeWidth: 2, opacity: 0.8 }, - '& .MuiAreaElement-series-Retained': { fill: "url('#myGradient2')", strokeWidth: 2, opacity: 0.8 }, + '& .MuiAreaElement-series-Current': { fill: "url('#myGradient2')", strokeWidth: 2, opacity: 0.8 }, '& .MuiChartsAxis-directionX .MuiChartsAxis-tick': { stroke: theme.palette.divider } }} > @@ -189,8 +191,6 @@ export default function UserAreaChart({ view, filters }) { Next - - )} diff --git a/src/sections/dashboard/default/UserRetentionMetrics.jsx b/src/sections/dashboard/default/UserRetentionMetrics.jsx index fb3f878..5e7a2f1 100644 --- a/src/sections/dashboard/default/UserRetentionMetrics.jsx +++ b/src/sections/dashboard/default/UserRetentionMetrics.jsx @@ -35,9 +35,11 @@ export default function UserRetentionMetrics({ filters }) { const [periodType, setPeriodType] = useState('month'); const today = new Date().toISOString().split('T')[0]; - const startDate = filters?.startDate || '2021-01-10'; + const startDate = filters?.startDate || '2020-01-10'; const endDate = filters?.endDate || today; const country = filters?.countryCode || ''; + const type = filters?.type || ''; + const origin = filters?.origin || ''; const calculateFuturePeriod = useCallback((basePeriod, periodsAhead) => { const date = dayjs(basePeriod); @@ -107,13 +109,15 @@ export default function UserRetentionMetrics({ filters }) { try { const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API; const countryParam = country ? `&country_code=${country}` : ''; + const typeParam = type ? `&type=${type}` : ''; + const originParam = origin ? `&origin=${origin}` : ''; const signupResponse = await axios.get( - `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}` + `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}${typeParam}${originParam}` ); const retainedResponse = await axios.get( - `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}` + `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}${typeParam}${originParam}` ); const signupData = signupResponse?.data?.signup?.data ?? []; @@ -132,7 +136,7 @@ export default function UserRetentionMetrics({ filters }) { }; fetchRetentionData(); - }, [startDate, endDate, country, processCohortData, processRetentionCurves]); + }, [startDate, endDate, country, type, origin, processCohortData, processRetentionCurves]); const getCellColor = (percentage) => { if (percentage >= 80) return theme.palette.success.dark; diff --git a/src/sections/dashboard/default/UserTable.jsx b/src/sections/dashboard/default/UserTable.jsx index 8320e2f..ca63498 100644 --- a/src/sections/dashboard/default/UserTable.jsx +++ b/src/sections/dashboard/default/UserTable.jsx @@ -65,7 +65,7 @@ export default function UserTable({ filters }) { const today = new Date(); const defaultEndDate = today.toISOString().split('T')[0]; - const defaultStartDate = '2021-01-10'; + const defaultStartDate = '2020-01-10'; const effectiveStartDate = filters?.startDate || defaultStartDate; const effectiveEndDate = filters?.endDate || defaultEndDate; const groupBy = filters?.groupBy || 'date'; @@ -73,10 +73,12 @@ export default function UserTable({ filters }) { useEffect(() => { setPage(0); - }, [effectiveStartDate, effectiveEndDate, effectiveCategory, filters?.countryCode]); + }, [effectiveStartDate, effectiveEndDate, effectiveCategory, filters?.countryCode, filters?.type, filters?.origin]); useEffect(() => { const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : ''; + const typeParam = filters?.type ? `&type=${filters.type}` : ''; + const originParam = filters?.origin ? `&origin=${filters.origin}` : ''; setLoading(true); setError(''); @@ -84,10 +86,10 @@ export default function UserTable({ filters }) { if (effectiveCategory === 'all') { Promise.all([ fetch( - `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}` + `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}` ), fetch( - `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}` + `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}` ) ]) .then(([signupRes, retainedRes]) => Promise.all([signupRes.json(), retainedRes.json()])) @@ -128,7 +130,7 @@ export default function UserTable({ filters }) { }) .finally(() => setLoading(false)); } else { - const apiUrl = `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}`; + const apiUrl = `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}`; fetch(apiUrl) .then((res) => res.json()) @@ -152,6 +154,8 @@ export default function UserTable({ filters }) { filters?.startDate, filters?.endDate, filters?.countryCode, + filters?.type, + filters?.origin, groupBy, effectiveCategory, page, diff --git a/src/sections/publications/PlatformDistributionChart.jsx b/src/sections/publications/PlatformDistributionChart.jsx index f3d06ec..70ab7e1 100644 --- a/src/sections/publications/PlatformDistributionChart.jsx +++ b/src/sections/publications/PlatformDistributionChart.jsx @@ -17,7 +17,7 @@ export default function PlatformDistributionChart({ filters }) { const today = new Date(); const defaultEndDate = today.toISOString().split('T')[0]; - const startDate = filters?.startDate || '2021-01-10'; + const startDate = filters?.startDate || '2020-01-10'; const endDate = filters?.endDate || defaultEndDate; const status = filters?.status || ''; const source = filters?.source || ''; diff --git a/src/sections/publications/PlatformSuccessRateChart.jsx b/src/sections/publications/PlatformSuccessRateChart.jsx index 5c2e9fa..888956b 100644 --- a/src/sections/publications/PlatformSuccessRateChart.jsx +++ b/src/sections/publications/PlatformSuccessRateChart.jsx @@ -88,7 +88,7 @@ export function PlatformSuccessRateSummary({ filters }) { const today = new Date(); const defaultEndDate = today.toISOString().split('T')[0]; - const startDate = filters?.startDate || '2021-01-10'; + const startDate = filters?.startDate || '2020-01-10'; const endDate = filters?.endDate || defaultEndDate; const source = filters?.source || ''; const country = filters?.country || ''; @@ -188,7 +188,7 @@ export default function PlatformSuccessRateChart({ filters }) { const today = new Date(); const defaultEndDate = today.toISOString().split('T')[0]; - const startDate = filters?.startDate || '2021-01-10'; + const startDate = filters?.startDate || '2020-01-10'; const endDate = filters?.endDate || defaultEndDate; const source = filters?.source || ''; const country = filters?.country || ''; diff --git a/src/sections/publications/PublicationChart.jsx b/src/sections/publications/PublicationChart.jsx index 506157a..49bff3b 100644 --- a/src/sections/publications/PublicationChart.jsx +++ b/src/sections/publications/PublicationChart.jsx @@ -23,7 +23,7 @@ export default function PublicationChart({ filters }) { const today = new Date(); const defaultEndDate = today.toISOString().split('T')[0]; - const startDate = filters?.startDate || '2021-01-10'; + const startDate = filters?.startDate || '2020-01-10'; const endDate = filters?.endDate || defaultEndDate; const platform = filters?.platform || ''; const status = filters?.status || ''; diff --git a/src/sections/publications/PublicationMap.jsx b/src/sections/publications/PublicationMap.jsx index d695df1..dda3722 100644 --- a/src/sections/publications/PublicationMap.jsx +++ b/src/sections/publications/PublicationMap.jsx @@ -31,7 +31,7 @@ export default function PublicationMap({ filters, selectedCountry, onCountrySele const geoJsonLayerRef = useRef(null); const today = new Date().toISOString().split('T')[0]; - const effectiveStartDate = filters?.startDate || '2021-01-10'; + const effectiveStartDate = filters?.startDate || '2020-01-10'; const effectiveEndDate = filters?.endDate || today; const effectivePlatform = filters?.platform || ''; const effectiveStatus = filters?.status || ''; diff --git a/src/sections/publications/UsageHeatmap.jsx b/src/sections/publications/UsageHeatmap.jsx index f0916d3..fe0fbd1 100644 --- a/src/sections/publications/UsageHeatmap.jsx +++ b/src/sections/publications/UsageHeatmap.jsx @@ -22,7 +22,7 @@ export default function UsageHeatmap({ filters }) { const [error, setError] = useState(''); const today = new Date().toISOString().split('T')[0]; - const startDate = filters?.startDate || '2021-01-10'; + const startDate = filters?.startDate || '2020-01-10'; const endDate = filters?.endDate || today; const platform = filters?.platform || ''; const status = filters?.status || '';