Merge branch 'fix/v1437-review' into release/v1.43.7

# Conflicts:
#	src/app/api/analytics/route.ts
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 06:20:42 +02:00
commit 45c027aee2
217 files changed
+3636 -2127

No files matched your search

+40 -12
View File
@@ -4203,10 +4203,13 @@ paths:
With `groupBy=day` on a cumulative type the rows are one sum per local day, and `limit` / `offset` page over
those days; every returned day is complete. `type=SLEEP_DURATION` returns one row per night, paged the same way
over nights. On both, `meta.total` counts the days or nights in the filtered window. A day-sum page takes
`limit` as asked (up to the list's 5000); a sleep page holds at most 1000 nights, and `meta.limit` echoes the
value applied.
those days; every returned day is complete, which is why `valueMin` / `valueMax` (a filter on single readings)
are refused with 422 beside `groupBy=day`. `type=SLEEP_DURATION` returns one row per night, paged the same way
over nights; every night of the page has a row, and a day whose only sleep is daytime naps carries `value` 0
with its naps in `napCount` / `napAsleepMinutes`. Its `dayKey` is the same day `/api/sleep/night?date=` answers
for. On both, `meta.total` counts the days or nights in the filtered window. A day-sum page takes `limit` as
asked (up to the list's 5000); a sleep page holds at most 1000 nights, and `meta.limit` echoes the value
applied.
parameters:
- in: query
name: type
@@ -4727,8 +4730,8 @@ paths:
source's stage segments (each with an absolute start/end), the per-stage breakdown, asleep/in-bed/awake totals,
the mid-sleep awakenings count, and same-wake-day naps surfaced separately. Session-clustered, keyed by the
local wake day, collapsed to one source via the sleep priority ladder so two sources never overlay. `date`
omitted returns the most recent night. A read-only view over existing per-stage SLEEP_DURATION rows — no schema,
no new measurement type."
omitted returns the most recent night holding sleep, however old. Any past date is answered, with no window cut.
A read-only view over existing per-stage SLEEP_DURATION rows — no schema, no new measurement type."
parameters:
- in: query
name: date
@@ -47896,7 +47899,9 @@ components:
- naps
additionalProperties: false
description: "One reconstructed sleep night: the main session's hypnogram segments + breakdown, plus same-wake-day naps
surfaced separately."
surfaced separately. A session is a nap when it holds under three hours asleep centred between 10:00 and 20:00
local time; the main session is the longest session that is not a nap. A day whose only sleep is naps has `main:
null` and its naps."
SleepRhythmResponse:
type: object
properties:
@@ -62987,14 +62992,30 @@ components:
- type: "null"
description: Non-null when two writer buckets reported clearly different asleep totals for the latest night's main
session. Observational only — the served summary stays the winning writer's totals.
recoveryEmpty:
anyOf:
- type: string
enum:
- general
- oura_permission
- oura_no_resilience
- type: "null"
description: "Why the recovery page has nothing to chart, resolved on the server; null when any recovery type
(ANS_CHARGE, DAY_STRAIN, WORKOUT_STRAIN, CARDIO_LOAD, AVERAGE_HEART_RATE, MAX_HEART_RATE,
ENERGY_EXPENDITURE_KJ, RESILIENCE) has a reading. `oura_permission`: Oura is connected and its grant leaves
out the scope the resilience level needs, one reconnect fixes it. `oura_no_resilience`: Oura is connected
and no level has arrived yet. `general`: anyone else. Clients render the empty note from this value and
never work it out from the connection status."
required:
- summaries
- bmi
- lastSeenByType
- sleepSourceDiscrepancy
- recoveryEmpty
additionalProperties: false
description: "What `?slice=summaries` answers: the per-type summaries and their freshness, and nothing thick. Four keys,
and the caller gets them from two SQL passes rather than the default slice's chain."
description: "What `?slice=summaries` answers: the per-type summaries and their freshness, the recovery page's empty
reason, and nothing thick. Five keys, and the caller gets them from two SQL passes rather than the default
slice's chain."
InsightsGenerateReadEnvelope:
type: object
properties:
@@ -88257,9 +88278,9 @@ components:
type: number
description: Effective weight after redistributing missing sub-scores.
gap:
description: "Only on a null `consistency` / `timing`: why it is withheld. `few_nights` — fewer than three nights in the
window have a main sleep on the clock (count in `rhythmNights`); `no_main_sleep` — the latest day had
only a daytime nap. Absent otherwise."
description: "Only on a null `consistency` / `timing`: why it is withheld. `few_nights` — fewer than `rhythmMinNights`
nights in the window have a main sleep on the clock (count in `rhythmNights`); `no_main_sleep` — the
latest day had only a daytime nap. Absent otherwise."
type: string
enum:
- few_nights
@@ -88281,6 +88302,12 @@ components:
maximum: 9007199254740991
description: Nights in the window with a main sleep placed on the clock — what consistency and timing are computed from.
A daytime nap is not one; a short night is.
rhythmMinNights:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
description: 'How many `rhythmNights` consistency and timing need before they are scored; below it they carry `gap:
"few_nights"`.'
series:
type: array
items:
@@ -88295,6 +88322,7 @@ components:
- subScores
- windowNights
- rhythmNights
- rhythmMinNights
- series
additionalProperties: false
parameters:
+4 -1
View File
@@ -265,7 +265,10 @@ reading:
nothing left but a data type waiting for its second attempt and queued the
next slice for when it is due (`delaySeconds`).
- `meta["googleHealth.backfill.ended"]` appears on the slice where a data
type ended short, with the same entries. A walk stopped for lack of
type ended short: the `collection`, its measurement `type`, the `reason`,
whether the end is `final`, how many data types have ended short so far
(`endedCount`) and of those for good (`finalCount`), and their `types`.
The full entries are in the slice report's `ended`. A walk stopped for lack of
progress also leaves a warning:
`fetchHeartRate: 20 pages in a row reached no further back than <instant>`.
- `meta["googleHealth.backfill.edgeFilterUnsupported"]` appears on the slice
+11 -1
View File
@@ -7,7 +7,7 @@ A Node process that reaches its heap limit stops with `JavaScript heap out of me
Two background wide events, no configuration needed:
- `runtime.heap.sampled` (info), every five minutes, and every minute while the heap is at or above 70 % of its limit. It goes to stdout and Loki but not to Admin > App logs, so it cannot push other events out of that page's 500-entry buffer. It carries `heap_used_mb`, `heap_total_mb`, `heap_limit_mb`, `heap_used_ratio`, `old_space_mb`, `rss_mb`, `external_mb`, `array_buffers_mb`, `active_requests`, `active_jobs`, `in_flight_dropped` and `uptime_s`.
- `runtime.heap.pressure` (warn), the first time the heap reaches 85 % of its limit. Besides the figures above it lists the 50 oldest requests and jobs in flight (`in_flight_oldest`: kind, method, route, age in seconds, whether it was a server action) and the routes most of them share (`in_flight_by_route`). Routes are reduced to their shape: no query string, secret path segments replaced by `[REDACTED]`, ids by `:id`, anything unusual by `:x`. It writes again only after the heap has dropped below 70 % and thirty minutes have passed.
- `runtime.heap.pressure` (warn), the first time the heap reaches 85 % of its limit. Besides the figures above it lists the 50 oldest requests and jobs in flight (`in_flight_oldest`: kind, method, route, age in seconds, whether it was a server action) and the routes most of them share (`in_flight_by_route`). Routes are reduced to their shape: no query string, secret path segments replaced by `[REDACTED]`, ids by `:id`, anything unusual by `:x`. It writes again only after the heap has dropped below 70 % and thirty minutes have passed. One exception: when the heap reaches 95 %, the same report is written again at once, with `pressure_ratio: 0.95`, whatever the cooldown; that tier re-arms as soon as the heap is back under 85 %. A heap that climbs to its limit shortly after an 85 % report therefore still leaves a report naming the work that pushed it over.
The heap is read every five seconds, a single call that takes microseconds. Tracking requests costs one map insert and one delete per request.
@@ -17,6 +17,16 @@ To find the last lines before a crash:
docker compose logs app --since 2h | grep '"runtime.heap.'
```
## The report Node writes when the heap runs out
The image sets `NODE_OPTIONS="--report-on-fatalerror --report-filename=stderr --report-exclude-env --report-exclude-network"`. When the process runs out of heap, Node writes a diagnostic report, about 10 KB of JSON with `"trigger": "OOMError"`, to stderr just before it exits: the JavaScript stack at the failed allocation, the native stack, the heap spaces and the open handles. It lands in the container log beside the `runtime.heap.*` lines:
```sh
docker compose logs app --since 2h | grep -A400 '"trigger": "OOMError"'
```
The two exclude flags keep environment variables (keys, passwords, the database URL) and network interfaces out of the report. If you set `NODE_OPTIONS` yourself, for example to raise `--max-old-space-size`, your value replaces the image's; add the four flags to it to keep the report.
## Optional heap snapshot
A snapshot shows what the heap is made of, which the events above cannot. Turn it on only while you are chasing a specific crash:
+5 -1
View File
@@ -79,9 +79,13 @@ Raise the limit on the container that runs the worker and re-upload:
services:
app:
environment:
NODE_OPTIONS: --max-old-space-size=1024
NODE_OPTIONS: --max-old-space-size=1024 --report-on-fatalerror --report-filename=stderr --report-exclude-env --report-exclude-network
```
Your `NODE_OPTIONS` replaces the image's, so carry the four `--report-*`
flags over: they make an out-of-memory exit leave a diagnostic report in
the container log (see `docs/ops/heap-diagnostics.md`).
An import interrupted by a container restart or an out-of-memory kill
does not stay stuck: the worker reconciles non-terminal import jobs at
boot and every 15 minutes, flipping orphaned rows to `failed`