Files
nextcloud-analytics/analyticshub/lib/Service/DataProcessor.php
WLTBAgent 8a445c4d46 Fix: Rename app folder to match app ID
- Renamed analytics-hub/ → analyticshub/
- App ID in info.xml is 'analyticshub' (no hyphen)
- Nextcloud requires folder name to match app ID exactly
- Fixes 'Could not download app analyticshub' error during installation

Installation:
- Upload analyticshub/ folder to /var/www/nextcloud/apps/
- Folder name must match app ID in info.xml
2026-02-13 18:21:39 +00:00

255 lines
7.9 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\AnalyticsHub\Service;
use OCA\AnalyticsHub\Model\ClientConfig;
use OCP\Util\Logger;
/**
* Data Processor Service
* Handles intelligent delta calculations and data validation
*/
class DataProcessor {
private ?Logger $logger;
// Default thresholds
private const DEFAULT_SIGNIFICANT_CHANGE_PCT = 20;
private const DEFAULT_SIGNIFICANT_CHANGE_ABS = 5;
public function __construct() {
$this->logger = \OC::$server->getLogger();
}
/**
* Process and validate GA4 data
*/
public function process(array $rawData, ClientConfig $client): array {
$this->logger->info("Processing data for: {$client->getName()}");
try {
// Validate data completeness
$this->validateCompleteness($rawData);
// Calculate deltas
$processed = $this->calculateDeltas($rawData);
// Apply client thresholds
$processed = $this->applyThresholds($processed, $client);
// Generate summary
$processed['summary'] = $this->generateSummary($processed);
return $processed;
} catch (\Exception $e) {
$this->logger->error("Data processing failed: {$e->getMessage()}");
throw $e;
}
}
/**
* Validate data completeness
*/
private function validateCompleteness(array $data): void {
$requiredKeys = ['dates', 'metrics'];
foreach ($requiredKeys as $key) {
if (!isset($data[$key])) {
throw new DataIncompleteException("Missing required key: {$key}");
}
}
// Check we have at least 7 days of data
if (!isset($data['dates']) || count($data['dates']) < 7) {
throw new DataIncompleteException("Insufficient data - need at least 7 days");
}
// Check for null or empty metrics
$metrics = $data['metrics'] ?? [];
$requiredMetrics = ['sessions', 'totalUsers', 'conversions', 'eventCount'];
foreach ($requiredMetrics as $metric) {
if (!isset($metrics[$metric])) {
throw new DataIncompleteException("Missing required metric: {$metric}");
}
$values = $metrics[$metric] ?? [];
foreach ($values as $value) {
if ($value === null || $value === '') {
throw new DataIncompleteException("Null or empty value in metric: {$metric}");
}
}
}
$this->logger->info('Data validation passed');
}
/**
* Calculate intelligent deltas
*/
private function calculateDeltas(array $data): array {
$metrics = $data['metrics'] ?? [];
$processed = [];
foreach ($metrics as $metric => $values) {
$processed[$metric] = $this->calculateMetricDelta($values);
}
$processed['dates'] = $data['dates'] ?? [];
return $processed;
}
/**
* Calculate delta for a single metric
*/
private function calculateMetricDelta(array $values): array {
if (count($values) < 2) {
return [
'change' => 0,
'change_pct' => 0,
'abs_change' => 0,
'is_significant' => false,
'label' => 'Insufficient data',
'trend' => 'flat'
];
}
$current = $values[count($values) - 1]; // Last 7 days
$previous = $values[0]; // First 7 days (comparison)
// Edge case 1: Division by zero
if ($previous == 0) {
if ($current == 0) {
return [
'change' => 0,
'change_pct' => 0,
'abs_change' => 0,
'is_significant' => false,
'label' => 'No change',
'trend' => 'stable'
];
} else {
return [
'change' => $current,
'change_pct' => null,
'abs_change' => $current,
'is_significant' => true,
'label' => "New activity (+{$current})",
'trend' => 'increasing'
];
}
}
// Calculate percentage change
$changePct = (($current - $previous) / $previous) * 100;
$absChange = abs($current - $previous);
// Edge case 2: Small numbers creating misleading %
// Require both % threshold AND minimum absolute change
$thresholds = [
'significant_change_pct' => self::DEFAULT_SIGNIFICANT_CHANGE_PCT,
'significant_change_abs' => self::DEFAULT_SIGNIFICANT_CHANGE_ABS
];
$isSignificant = (abs($changePct) > $thresholds['significant_change_pct']
&& $absChange > $thresholds['significant_change_abs']);
// Determine trend
$trend = 'stable';
if ($absChange > $thresholds['significant_change_abs']) {
$trend = $changePct > 0 ? 'increasing' : 'decreasing';
}
return [
'current' => $current,
'previous' => $previous,
'change' => $current - $previous,
'change_pct' => round($changePct, 1),
'abs_change' => $absChange,
'is_significant' => $isSignificant,
'label' => $this->formatDeltaLabel($changePct, $absChange, $isSignificant),
'trend' => $trend
];
}
/**
* Format delta label
*/
private function formatDeltaLabel(float $changePct, float $absChange, bool $isSignificant): string {
if (!$isSignificant) {
return 'No significant change';
}
$direction = $changePct > 0 ? '+' : '';
$pct = abs($changePct);
$change = (int)$absChange;
return "{$direction}{$pct}% ({$direction}{$change})";
}
/**
* Apply client-specific thresholds
*/
private function applyThresholds(array $processed, ClientConfig $client): array {
$thresholds = $client->getThresholds();
if (!$thresholds) {
// Use defaults
$processed['significant_change_pct'] = self::DEFAULT_SIGNIFICANT_CHANGE_PCT;
$processed['significant_change_abs'] = self::DEFAULT_SIGNIFICANT_CHANGE_ABS;
} else {
// Use client thresholds
$processed['significant_change_pct'] = $thresholds['significant_change_pct'] ?? self::DEFAULT_SIGNIFICANT_CHANGE_PCT;
$processed['significant_change_abs'] = $thresholds['significant_change_abs'] ?? self::DEFAULT_SIGNIFICANT_CHANGE_ABS;
}
// Re-evaluate significance based on thresholds
foreach ($processed as $metric => $data) {
if (is_array($data)) {
$data['is_significant'] = (
abs($data['change_pct'] ?? 0) > $processed['significant_change_pct']
&& abs($data['abs_change'] ?? 0) > $processed['significant_change_abs']
);
}
}
return $processed;
}
/**
* Generate summary
*/
private function generateSummary(array $processed): array {
$totalMetrics = 0;
$significantChanges = 0;
$increasing = 0;
$decreasing = 0;
foreach ($processed as $metric => $data) {
if (is_array($data)) {
$totalMetrics++;
if ($data['is_significant'] ?? false) {
$significantChanges++;
}
if (($data['trend'] ?? '') === 'increasing') {
$increasing++;
}
if (($data['trend'] ?? '') === 'decreasing') {
$decreasing++;
}
}
}
return [
'total_metrics' => $totalMetrics,
'significant_changes' => $significantChanges,
'increasing' => $increasing,
'decreasing' => $decreasing
];
}
}