Phase 3: Initial commit - Nextcloud Analytics Hub Project
Nextcloud Analytics Hub complete: - Nextcloud PHP app (analytics-hub/) - All phases (1-3) complete - Go client tool (nextcloud-analytics) - Full CLI implementation - Documentation (PRD, README, STATUS, SKILL.md) - Production-ready for deployment to https://cloud.shortcutsolutions.net Repository: git.teamworkapps.com/shortcut/nextcloud-analytics Workspace: /home/molt/.openclaw/workspace
This commit is contained in:
254
analytics-hub/lib/Service/DataProcessor.php
Normal file
254
analytics-hub/lib/Service/DataProcessor.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?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
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user