Add Health import failure diagnostics
This commit is contained in:
+153
-5
@@ -266,10 +266,14 @@ final class App
|
|||||||
}
|
}
|
||||||
|
|
||||||
$username = (string) ($user['username'] ?? '');
|
$username = (string) ($user['username'] ?? '');
|
||||||
$payload = request_json_body();
|
$rawBody = (string) file_get_contents('php://input');
|
||||||
|
$payload = $this->decodeHealthImportPayload($rawBody);
|
||||||
if ($payload === []) {
|
if ($payload === []) {
|
||||||
$this->users->recordHealthImport($username, 'error', 'Leerer oder ungültiger JSON-Import.');
|
$traceID = $this->healthImportTraceID();
|
||||||
json_response(['ok' => false, 'message' => 'Leerer oder ungültiger JSON-Import.'], 400);
|
$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 {
|
try {
|
||||||
@@ -285,11 +289,55 @@ final class App
|
|||||||
$this->users->recordHealthImport($username, 'ok', $message);
|
$this->users->recordHealthImport($username, 'ok', $message);
|
||||||
json_response(['ok' => true, 'message' => $message, 'result' => $result]);
|
json_response(['ok' => true, 'message' => $message, 'result' => $result]);
|
||||||
} catch (RuntimeException $exception) {
|
} catch (RuntimeException $exception) {
|
||||||
$this->users->recordHealthImport($username, 'error', $exception->getMessage());
|
$traceID = $this->healthImportTraceID();
|
||||||
json_response(['ok' => false, 'message' => $exception->getMessage()], 400);
|
$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
|
private function handleHealthImportStatus(): void
|
||||||
{
|
{
|
||||||
$user = $this->requireUser();
|
$user = $this->requireUser();
|
||||||
@@ -569,6 +617,106 @@ final class App
|
|||||||
return implode('. ', $parts) . '.';
|
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
|
private function healthEventsFromMetrics(array $metrics): array
|
||||||
{
|
{
|
||||||
$steps = [];
|
$steps = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user