Add Health import failure diagnostics

This commit is contained in:
2026-05-19 15:30:43 +02:00
parent d8636f6c41
commit 176b07f202
+153 -5
View File
@@ -266,10 +266,14 @@ final class App
}
$username = (string) ($user['username'] ?? '');
$payload = request_json_body();
$rawBody = (string) file_get_contents('php://input');
$payload = $this->decodeHealthImportPayload($rawBody);
if ($payload === []) {
$this->users->recordHealthImport($username, 'error', 'Leerer oder ungültiger JSON-Import.');
json_response(['ok' => false, 'message' => 'Leerer oder ungültiger JSON-Import.'], 400);
$traceID = $this->healthImportTraceID();
$message = 'Diagnose-ID: ' . $traceID . '. Leerer oder ungültiger JSON-Import.';
$this->logHealthImportFailure($traceID, $username, 'Leerer oder ungültiger JSON-Import.', [], strlen($rawBody));
$this->users->recordHealthImport($username, 'error', $message);
json_response(['ok' => false, 'message' => $message], 400);
}
try {
@@ -285,11 +289,55 @@ final class App
$this->users->recordHealthImport($username, 'ok', $message);
json_response(['ok' => true, 'message' => $message, 'result' => $result]);
} catch (RuntimeException $exception) {
$this->users->recordHealthImport($username, 'error', $exception->getMessage());
json_response(['ok' => false, 'message' => $exception->getMessage()], 400);
$traceID = $this->healthImportTraceID();
$message = 'Diagnose-ID: ' . $traceID . '. ' . $exception->getMessage();
$this->logHealthImportFailure($traceID, $username, $exception->getMessage(), $payload, strlen($rawBody));
$this->users->recordHealthImport($username, 'error', $message);
json_response(['ok' => false, 'message' => $message], 400);
}
}
private function decodeHealthImportPayload(string $rawBody): array
{
if (trim($rawBody) === '') {
return [];
}
$decoded = json_decode($rawBody, true);
return is_array($decoded) ? $decoded : [];
}
private function healthImportTraceID(): string
{
try {
return substr(bin2hex(random_bytes(4)), 0, 8);
} catch (Exception) {
return substr(sha1((string) microtime(true)), 0, 8);
}
}
private function logHealthImportFailure(string $traceID, string $username, string $message, array $payload, int $rawLength): void
{
$metrics = $payload === [] ? [] : $this->healthMetricsFromPayload($payload);
$workouts = $payload === [] ? [] : $this->healthWorkoutsFromPayload($payload);
$context = [
'trace_id' => $traceID,
'user' => $username,
'message' => $message,
'method' => (string) ($_SERVER['REQUEST_METHOD'] ?? ''),
'path' => (string) ($_SERVER['REQUEST_URI'] ?? ''),
'content_type' => (string) ($_SERVER['CONTENT_TYPE'] ?? ($_SERVER['HTTP_CONTENT_TYPE'] ?? '')),
'content_length' => (string) ($_SERVER['CONTENT_LENGTH'] ?? ''),
'raw_length' => $rawLength,
'json_error' => json_last_error_msg(),
'user_agent' => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''),
'payload' => $this->healthPayloadDiagnostics($payload, $metrics, $workouts),
];
error_log('[Mood Health Import] ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
private function handleHealthImportStatus(): void
{
$user = $this->requireUser();
@@ -569,6 +617,106 @@ final class App
return implode('. ', $parts) . '.';
}
private function healthPayloadDiagnostics(array $payload, array $metrics, array $workouts): array
{
return [
'top_level' => $this->describeHealthPayloadNode($payload),
'metrics_count' => count($metrics),
'workouts_count' => count($workouts),
'metric_samples' => $this->healthMetricDiagnostics($metrics),
'workout_samples' => $this->healthArraySamples($workouts),
'payload_samples' => $this->healthNestedArraySamples($payload),
];
}
private function healthMetricDiagnostics(array $metrics): array
{
$samples = [];
foreach (array_slice($metrics, 0, 8) as $index => $metric) {
if (!is_array($metric)) {
continue;
}
$data = is_array($metric['data'] ?? null) ? $metric['data'] : [];
$firstPoint = null;
foreach ($data as $point) {
if (is_array($point)) {
$firstPoint = $point;
break;
}
}
$samples[] = [
'index' => $index,
'name' => (string) ($metric['name'] ?? ''),
'normalized' => $this->healthMetricName((string) ($metric['name'] ?? '')),
'data_count' => count($data),
'keys' => array_slice(array_keys($metric), 0, 12),
'first_point' => is_array($firstPoint) ? $this->describeHealthPayloadNode($firstPoint) : null,
];
}
return $samples;
}
private function healthArraySamples(array $items): array
{
$samples = [];
foreach (array_slice($items, 0, 5) as $index => $item) {
if (is_array($item)) {
$samples[] = ['index' => $index] + $this->describeHealthPayloadNode($item);
}
}
return $samples;
}
private function healthNestedArraySamples(array $payload, string $path = '$', int $depth = 0): array
{
if ($depth > 2) {
return [];
}
$samples = [];
foreach ($payload as $key => $value) {
$currentPath = $path . (is_int($key) ? '[' . $key . ']' : '.' . (string) $key);
if (!is_array($value)) {
continue;
}
$samples[] = ['path' => $currentPath] + $this->describeHealthPayloadNode($value);
if (count($samples) >= 12) {
break;
}
foreach ($this->healthNestedArraySamples($value, $currentPath, $depth + 1) as $nested) {
$samples[] = $nested;
if (count($samples) >= 12) {
break 2;
}
}
}
return $samples;
}
private function describeHealthPayloadNode(array $node): array
{
$keys = array_slice(array_keys($node), 0, 12);
$types = [];
foreach ($keys as $key) {
$value = $node[$key] ?? null;
$types[(string) $key] = is_array($value) ? 'array(' . count($value) . ')' : get_debug_type($value);
}
return [
'is_list' => array_is_list($node),
'count' => count($node),
'keys' => $keys,
'types' => $types,
];
}
private function healthEventsFromMetrics(array $metrics): array
{
$steps = [];