Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cfef569
perf(analytics): bound power curve builds
Asherlc Jul 26, 2026
b69e412
test(analytics): update power eligibility fixture
Asherlc Jul 26, 2026
e81b6ed
docs(ops): record SAST registry outage
Asherlc Jul 26, 2026
c51911e
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
c6beae1
fix(analytics): preserve bounded power progress
Asherlc Jul 26, 2026
f86b76e
fix(analytics): order ClickHouse FINAL clause
Asherlc Jul 26, 2026
b59a46e
test(analytics): isolate power progress fixtures
Asherlc Jul 26, 2026
6c9446c
test(analytics): match power lifecycle schema
Asherlc Jul 26, 2026
33b6dc1
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
ea8efc6
fix(mobile): defer background WHOOP init
Asherlc Jul 26, 2026
d0d3d7f
fix(mobile): serialize initial WHOOP sync
Asherlc Jul 26, 2026
d13fe4f
fix(mobile): stop WHOOP sync on background
Asherlc Jul 26, 2026
8177587
test(mobile): await WHOOP stream startup
Asherlc Jul 26, 2026
de3ed01
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
c6fb426
fix(analytics): accept current dbt statuses
Asherlc Jul 26, 2026
6b14d54
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
a867312
perf(server): use monthly serving models
Asherlc Jul 26, 2026
3783b5e
refactor(server): drop report timezone input
Asherlc Jul 26, 2026
5aab1d4
test(server): cover report serving lifecycle
Asherlc Jul 26, 2026
f833747
docs(perf): record report query evidence
Asherlc Jul 26, 2026
fbdb8db
test(server): verify report query bindings
Asherlc Jul 26, 2026
4342648
fix(worker): bound jobs during deploys
Asherlc Jul 26, 2026
4783652
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
8a472df
test(providers): expect request deadlines
Asherlc Jul 26, 2026
6fa5765
test(provider-http): verify timeout cause
Asherlc Jul 26, 2026
9ab4b20
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
81a02f3
fix(worker): keep deletion status durable
Asherlc Jul 26, 2026
0cae052
refactor(worker): isolate deletion wiring
Asherlc Jul 26, 2026
d2a8204
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
ef80ccf
test(worker): type deletion continuation
Asherlc Jul 26, 2026
1cd039d
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
36b3dc8
fix(deploy): serialize production rollouts
Asherlc Jul 26, 2026
615969b
fix(deploy): prevent stale queued rollouts
Asherlc Jul 26, 2026
48b99c1
fix(deploy): check live branch freshness
Asherlc Jul 26, 2026
0234213
fix(deploy): gate the production lock
Asherlc Jul 26, 2026
5e5e623
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
921659a
Merge remote-tracking branch 'origin/main' into Asherlc/use-subagents
Asherlc Jul 26, 2026
f4cb34e
fix(peloton): accept nullable workout fields
Asherlc Jul 26, 2026
e86e116
test(peloton): share the response schema
Asherlc Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/production-incident-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -18405,3 +18405,36 @@ Drizzle schema and runtime Zod schemas. Findings and remediations:
old worker reaches `Shutdown complete` without exit 137, verify the timed-out
Strava job retries and completes, observe no fixed-release 4N/2K events
across a subsequent rollout, then resolve both Sentry issues.

## 2026-07-26 — Peloton Workout Response Drift Blocked Sync

- **Status:** Direct source fix validated locally; merge and production
deployment pending.
- **Symptoms:** Scheduled Peloton syncs failed at the client response boundary
in
[Sentry issue DOFEK-SERVER-5E](https://east-bay-software.sentry.io/issues/DOFEK-SERVER-5E).
- **User impact:** The affected Peloton connection could not import any workout
page because validation rejected the complete page before parsing began.
- **Evidence:** Five consecutive production events from `14:30` through
`16:30` UTC reported the same response drift. Peloton returned `null` for
workout `title`, `end_time`, `metrics_type`, `peloton_id`, and `strava_id`,
and omitted `ride.instructor.id`; the runtime schema accepted only omitted
optional strings and required a numeric end time and instructor ID.
- **Root cause:** The client boundary modeled several optional upstream fields
as undefined-only even though the observed API payload uses both JSON `null`
and omission for absence. The schema therefore rejected valid unfinished and
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
partially linked workouts before the provider could normalize them.
- **Fix / mitigation:** Accept only the observed nullability and omission:
nullable title and source-link fields, nullable-but-present `end_time`, and
an optional instructor ID. Keep every other identity and timestamp field
required, preserve the upstream nulls in the client response, and normalize
a null end time to the existing unfinished-workout behavior only in the
parsed provider DTO.
- **Validation:** A production-shaped regression first failed with the exact
six Zod paths reported by Sentry. After the schema correction, all 17 focused
client and parsing tests pass, the root TypeScript typecheck passes, targeted
Biome checks report no findings, and the changed parser kills all 13
generated mutants.
- **Remaining risk / follow-up:** Merge through normal CI, deploy, observe a
successful scheduled Peloton sync on the fixed release, and then resolve
DOFEK-SERVER-5E.
39 changes: 39 additions & 0 deletions packages/peloton-client/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,45 @@ describe("PelotonClient", () => {
await expect(unavailable.getWorkouts()).rejects.toBeInstanceOf(PelotonServiceError);
});

it("accepts observed nullable workout fields and missing instructor IDs", async () => {
const response = {
data: [
{
id: "workout-1",
status: "COMPLETE",
fitness_discipline: "cycling",
title: null,
created_at: 1_709_280_000,
start_time: 1_709_280_000,
end_time: null,
total_work: 0,
is_total_work_personal_record: false,
metrics_type: null,
peloton_id: null,
strava_id: null,
ride: {
id: "ride-1",
title: "Recovery Ride",
duration: 1_800,
instructor: { name: "Coach" },
},
},
],
total: 1,
count: 1,
page: 0,
limit: 20,
page_count: 1,
sort_by: "-created_at",
show_next: false,
show_previous: false,
};
const responses = [Response.json({ id: "user-123" }), Response.json(response)];
const client = new PelotonClient("secret", async () => responses.shift() ?? Response.error());

await expect(client.getWorkouts()).resolves.toEqual(response);
});

it("requests and validates performance graphs", async () => {
const graph = {
duration: 5,
Expand Down
13 changes: 13 additions & 0 deletions packages/peloton-client/src/parsing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ describe("parseWorkout", () => {

expect(parsed.raw.instructor).toBeUndefined();
});

it("treats nullable completion and source identifiers as absent", () => {
const parsed = parseWorkout({
...workout,
end_time: null,
peloton_id: null,
strava_id: null,
});

expect(parsed.endedAt).toBeUndefined();
expect(parsed.stravaId).toBeUndefined();
expect(parsed.raw.pelotonClassId).toBeUndefined();
});
});

describe("parsePerformanceGraph", () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/peloton-client/src/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export interface ParsedPelotonWorkout {

export function parseWorkout(workout: PelotonWorkout): ParsedPelotonWorkout {
const startedAt = new Date(workout.start_time * 1000);
const endedAt = workout.end_time > 0 ? new Date(workout.end_time * 1000) : undefined;
const endTime = workout.end_time ?? 0;
const endedAt = endTime > 0 ? new Date(endTime * 1000) : undefined;
const stravaId = workout.strava_id && workout.strava_id !== "-1" ? workout.strava_id : undefined;

return {
Expand Down
12 changes: 6 additions & 6 deletions packages/peloton-client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";

export const pelotonInstructorSchema = z.object({
id: z.string(),
id: z.string().optional(),
name: z.string(),
image_url: z.string().optional(),
});
Expand All @@ -21,21 +21,21 @@ export const pelotonWorkoutSchema = z.object({
status: z.string(),
fitness_discipline: z.string(),
name: z.string().optional(),
title: z.string().optional(),
title: z.string().nullish(),
created_at: z.number(),
start_time: z.number(),
end_time: z.number(),
end_time: z.number().nullable(),
total_work: z.number(),
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
is_total_work_personal_record: z.boolean(),
metrics_type: z.string().optional(),
metrics_type: z.string().nullish(),
device_type: z.string().optional(),
platform: z.string().optional(),
peloton_id: z.string().optional(),
peloton_id: z.string().nullish(),
workout_type: z.string().optional(),
has_pedaling_metrics: z.boolean().optional(),
has_leaderboard_metrics: z.boolean().optional(),
timezone: z.string().optional(),
strava_id: z.string().optional(),
strava_id: z.string().nullish(),
ride: pelotonRideSchema.optional(),
total_leaderboard_users: z.number().optional(),
leaderboard_rank: z.number().optional(),
Expand Down
Loading