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 ]; } }