Skip to content

Telemetry Cloud Contract for BionicScout

Last updated: 2026-03-27 13:40 ET Owner: BionicLoop app team (contract) + BionicScout backend team (handler implementation)

Purpose

This is the canonical backend handoff contract for implementing /v1/telemetry handlers in BionicScout.

Use this document as the single source for: - envelope validation - event schema validation by event_type + schema_version - idempotent persistence - routing to analytics/timeline storage and CloudWatch log streams

For broader roadmap context, see: - Docs/Planning/ExecutionPlan.md (Workstream J) - Docs/Planning/TelemetryCloudIntegrationPlan.md

Phase 1 Contract Lock Baseline (Frozen)

The following is locked for schema_version = 1.0.0 as of 2026-03-09: - Envelope identity/correlation fields in this document are mandatory for app-side emitters. - Event-family payload minimums in this document are the canonical contract for backend validators. - loop.step.executed must carry execution semantics via step_executed_at and applied clinical config fields (applied_target_mgdl, applied_meal_upfront_percent, applied_tmax_minutes) when available. - Critical UI telemetry naming is locked to snake_case screen_id, element_id, and reason codes. - New event types or payload field removals require explicit contract version increment and migration notes.

Scope and Auth

  • Endpoint: POST /v1/telemetry
  • Auth: JWT Bearer token
  • Required scope: bionicscout.dev.api/telemetry.ingest
  • Reject unauthorized/forbidden requests before payload parsing.

Canonical Envelope (Required)

All events must include these fields:

{
  "event_type": "loop.step.executed",
  "schema_version": "1.0.0",
  "subject_id": "SUBJECT-001",
  "auth_user_sub": "cognito-user-sub",
  "created_at": "2026-02-21T21:10:00.000Z",
  "event_id": "uuid",
  "session_id": "uuid",
  "app_version": "1.0",
  "build_number": "1234",
  "app_env": "dev",
  "payload": {}
}

Required envelope rules: - event_type: non-empty string - schema_version: currently 1.0.0 - subject_id: non-empty string (UNSET allowed in development) - auth_user_sub: Cognito sub string from authenticated identity; UNSET allowed only for unauthenticated continuity mode - created_at: ISO-8601 UTC timestamp - event_id: UUID string (idempotency key component) - session_id: UUID string - app_env: dev|staging|prod - payload: JSON object

Identity trust rule: - Backend must treat JWT-authenticated sub as source of truth. - If client payload auth_user_sub is present and does not match JWT sub, reject or quarantine by policy. - Persist server-derived auth_user_sub for query/indexing regardless of client payload value.

Idempotency and Persistence Contract

Primary idempotency key: - (subject_id, event_id)

Handler behavior: 1. If key is new: persist event, return 202 accepted. 2. If key already exists: do not create duplicate; return 202 accepted with existing ingest metadata.

Persistence expectation: - Store raw envelope for forensic replay. - Store normalized event row for querying/timelines. - Include ingest metadata: ingest_id, received_at, ingest_version, validation status.

J5 Backend Implementation Packet (Schema + Idempotency)

This section is the implementation target for Workstream J5 in BionicScout.

Required validation pipeline order

  1. JWT auth + scope gate (bionicscout.dev.api/telemetry.ingest)
  2. Envelope field/type validation
  3. Event schema validation by (event_type, schema_version)
  4. Idempotency evaluation on (subject_id, event_id)
  5. Persistence transaction:
  6. raw envelope write
  7. normalized timeline projection write
  8. optional fanout (app.log.batch)
  9. Response generation (202 accepted, deduped semantics)

Canonical error body (backend)

{
  "error": {
    "code": "invalid_payload_schema",
    "message": "payload failed schema validation",
    "details": {
      "field": "payload.units_requested",
      "expected": "number",
      "actual": "string"
    }
  }
}

Error code policy

  • invalid_envelope (400)
  • unsupported_event_type (400)
  • unsupported_schema_version (400)
  • invalid_payload_schema (400)
  • auth_sub_mismatch (400 or 403 per security policy; must be deterministic)
  • idempotency_conflict (409) for same (subject_id,event_id) with non-identical payload hash
  • persistence_failed (500)

Idempotency behavior contract

  1. First-seen key (subject_id,event_id):
  2. persist event
  3. return 202, deduped=false
  4. Exact replay (same key, same payload hash):
  5. do not duplicate
  6. return 202, deduped=true, preserve original ingest_id
  7. Key collision with different payload (same key, different hash):
  8. do not overwrite prior record
  9. return 409 with error.code = idempotency_conflict

app.log.batch fanout contract

  • Fanout only after envelope+schema validation succeeds.
  • Preserve correlation metadata in CloudWatch records:
  • subject_id, auth_user_sub, event_id, session_id, app_env, created_at
  • Enforce metadata allowlist for entries[].metadata keys.
  • Unknown metadata keys are dropped (not fatal) and counted in observability metrics.

Allowlisted integration-test metadata keys for app.log.batch.entries[].metadata:

  • test_run_id
  • test_session_mode
  • test_upload_level
  • test_session_started_at_utc
  • test_session_expires_at_utc
  • test_event_at_utc
  • test_scenario_label
  • tester_name
  • test_stop_reason

J5 contract test matrix (backend must pass)

ID Scenario Expected
CT-J5-001 Valid envelope + valid payload 202, deduped=false, persisted raw + normalized rows
CT-J5-002 Replay exact same envelope 202, deduped=true, same ingest_id, no duplicate rows
CT-J5-003 Replay same (subject_id,event_id) with changed payload 409, error.code=idempotency_conflict, prior record unchanged
CT-J5-004 Missing required envelope field 400, error.code=invalid_envelope
CT-J5-005 Unknown event_type 400, error.code=unsupported_event_type
CT-J5-006 Unsupported schema_version 400, error.code=unsupported_schema_version
CT-J5-007 Payload type mismatch vs schema 400, error.code=invalid_payload_schema
CT-J5-008 Missing telemetry scope 403
CT-J5-009 JWT sub != payload auth_user_sub deterministic reject/quarantine behavior per policy
CT-J5-010 Simulated storage exception 500, error.code=persistence_failed
CT-J5-011 Valid app.log.batch with allowlisted metadata 202, CloudWatch fanout written with correlation metadata
CT-J5-012 app.log.batch with disallowed metadata keys 202, keys stripped, validation metric incremented

Event Families and Required Payload Fields

App Source Mapping Matrix (Emitter -> Event Family)

App component Event types emitted
BionicLoop/App/BionicLoopApp.swift app.lifecycle.*, alert.notification.tapped
BionicLoop/App/ContentView.swift auth.session.*
BionicLoop/Runtime/LoopRuntimeEngine.swift loop.session.*, loop.step.*, loop.command.*, alert.notification.scheduled, alert.notification.cleared
BionicLoop/Features/CGM/G7ViewModel.swift cgm.reading.*, cgm.connection.changed, cgm.state.changed
BionicLoop/Integrations/Pump/PumpStatusObserver.swift pump.connection.changed, pump.status.refreshed, pump.command.result
BionicLoop/Integrations/Pump/AppPumpManagerDelegate.swift pump.pod.lifecycle
BionicLoop/App/AuthSessionNetworking.swift (CloudLogUploadLogger, DeviceClockSyncMonitor) app.log.batch, lifecycle clock-check context fields

1) App/Auth lifecycle

  • app.lifecycle.launched
  • app.lifecycle.foregrounded
  • app.lifecycle.backgrounded
  • auth.session.authenticated
  • auth.session.signed_out
  • auth.session.restore_failed

Payload minimum: - source or reason when available - device_timezone_id (IANA timezone ID) - device_utc_offset_seconds - clock_check_result (ok|skewed|unavailable) - clock_check_skew_seconds (when available) - clock_check_rtt_ms (when available) - clock_check_at_utc (when available) - semantic guard: on NSSystemTimeZoneDidChange and UIApplication.significantTimeChangeNotification, app emits app.lifecycle.foregrounded with reason = timezone_or_time_changed and refreshed clock-check fields.

2) Loop/runtime/algorithm

  • loop.session.armed
  • loop.session.reset
  • loop.step.executed
  • loop.step.skipped
  • loop.command.requested
  • loop.command.applied
  • loop.command.blocked
  • algorithm.session.snapshot
  • algorithm.step.snapshot

loop.step.* payload minimum: - expected_step, executed_step, step_executed_at, wake_cause, skip_reason, recommendation_applied - optional snapshots: algorithm_input_snapshot, algorithm_output_snapshot - implemented skip reasons now include mealSlotConflict for meal-submit slot conflicts where another step advanced before coordinator acceptance; cloud reconstruction should treat this as explicit blocked meal intent rather than a generic cadence-only skip.

loop.command.* payload minimum: - step, command_type, units_requested, units_delivered, apply_result, reason - implemented app payload also carries command_outcome (applied, blocked, uncertain) so cloud reconstruction can distinguish ambiguous delivery from ordinary blocked command outcomes. - semantic guard: loop.command.blocked is emitted only when a loop recommendation/command existed for the step but could not be applied; cadence-only skips (stepNotDue, missingFreshGlucose without command) are not command-block events.

algorithm.session.snapshot payload minimum: - subject_id, session_id, event_id, session_started_at_utc, timezone_id, app_version, build_number - algorithm_name, algorithm_build_id?, pump_id? - subject_weight_kg, real_t0_seconds, step_interval_seconds - set_point_nominal_mgdl, set_point_mgdl, tmax_minutes, meal_upfront_percent - constants block for algorithm/session constants

algorithm.step.snapshot payload minimum: - subject_id, session_id, event_id, executed_step, step_executed_at, timezone_id, app_version, build_number - bp full matrix row object - bp_log typed object for currently implemented families: - a - i - g - tildeI - tildeG - adaptation (AD + G24h0/G24hWt) - pa - p - b - c - d - s - contract note: algorithm_build_id and pump_id remain nullable/open app-side fields and must not be inferred/fabricated downstream.

3) CGM

  • cgm.reading.processed
  • cgm.reading.masked
  • cgm.connection.changed
  • cgm.state.changed

Payload minimum: - reading_timestamp - reliable - has_sensor - value_mgdl (when reliable) - trend (when available) - mask_reason (when masked) - source_state - semantic guard: cgm.state.changed must reflect post-refresh/current callback state (not stale pre-refresh cached state). - semantic guard: cgm.connection.changed status_text must be derived from refreshed/current lifecycle state (no stale pre-refresh status labels).

4) Pump

  • pump.connection.changed
  • pump.status.refreshed
  • pump.command.result
  • pump.pod.lifecycle

Payload minimum: - delivery_state - reservoir_level_u or reservoir_units - units_requested, units_delivered (command/result) - pod_active - error_code (when applicable) - schema guard: pump.command.result uses pump/result payload fields only (units_requested, units_delivered, delivery_state, result metadata) and is not reused for loop-command-block payload shape. - emission guard: pump.command.result is emitted only on command-result state/progress deltas (request_step, units_requested, units_delivered, or delivery_state change), not on repeated unchanged refresh snapshots.

5) Alerts

  • alert.issued
  • alert.retracted
  • alert.acknowledged
  • alert.notification.scheduled
  • alert.notification.cleared
  • alert.notification.tapped

Payload minimum: - alert_code, severity, source, dedupe_key - requires_acknowledge, ack_state - message (issued paths) - title, recommended_action (issued/updated/cleared parity payload) - current app-level actionable alerts include ALERT-AUTH-LOGIN-REQUIRED, ALERT-SUBJECT-ID-CONFLICT, and ALERT-APP-CLOCK-SKEW. - lifecycle edge rule: alert.notification.cleared is emitted only when an active alert is actually removed; repeated retract calls on absent dedupe keys are intentionally no-op for notification-cleared telemetry - lifecycle side-effect rule: OS notification clear is attempted on every retract call (idempotent clear), even when no active in-memory alert remains, to handle async schedule/retract races.

6) Critical UI interactions (schema locked; implementation in progress)

  • ui.critical.tap
  • ui.critical.submit
  • ui.critical.cancel
  • ui.critical.blocked
  • ui.critical.state_viewed

Payload minimum: - screen_id, element_id, action, result - reason for blocked/failed - flow_id for multi-step flows - contract note: Home manual BG tap uses canonical element_id = home.bg_button - contract note: Home manual BG submit is runtime-authoritative only - contract note: meal ui.critical.submit is a correlated lifecycle keyed by flow_id: submitted on user confirm, followed by runtime-emitted accepted / success / blocked / uncertain / resolved transitions; resolved may arrive after relaunch reconciliation or session reset using the persisted meal flow ID + target step - contract note: participant target-change approval flow uses stable screen_id = settings and element_id values under settings.target_change.*; payload details include current_target_mgdl, requested_target_mgdl, target_range_profile, approver_name, and approval_time_utc when submitted

Locked reason-code baseline for currently instrumented critical flows: - meal availability/actions: loop_off, missing_profile, no_pump, awaiting_first_step, too_early, too_late, pump_delivering, missing_fresh_glucose, signal_loss, request_in_progress, unavailable - manual BG/actions: loop_off, awaiting_first_step, missing_config_or_pump, rejected_value - clinical settings save flow: locked, invalid_profile, no_changes, subject_id_conflict, auth_required, claim_retryable - participant target-change approval flow: target_outside_allowed_range, no_change, missing_approver_name, missing_approval_time - modal/system action: app_background

7) Telemetry transport health

  • telemetry.outbox.enqueued
  • telemetry.flush.started
  • telemetry.flush.succeeded
  • telemetry.flush.failed
  • telemetry.event.dropped

Payload minimum: - queue_depth, retry_count, error_class/error_code, dropped_count, reason - flush guardrail: if a flush request arrives while a flush is active, reporter records a deferred follow-up flush request and executes it immediately after the active pass to avoid stranded queue entries.

8) Structured app logs

  • app.log.batch

Payload minimum: - threshold - source - entries[] where each entry has: - timestamp, level, subsystem, category, messageTemplate, metadata

Current app sources for app.log.batch: - source = bionicloop.app (structured app logger) - source = bionicloop.integration_test_session (integration test session markers) - source = bionicloop.algo2015.bp_log (Algo2015 native diagnostics tailer) - source = bionicloop.algo2015.bp_matrix (Algo2015 BP matrix diagnostics tailer)

Current Algo2015 metadata keys in entries[].metadata: - bp_log_file - line_prefix - step_hint (when parseable from STEP=, *OUT: L_BP=, or prefix-record tokens) - optional upload-cap summary keys: - dropped_line_count - retained_line_count - upload_cap

Current integration-test metadata keys in entries[].metadata: - test_run_id - test_session_mode - test_upload_level - test_session_started_at_utc - test_session_expires_at_utc - test_event_at_utc - optional: - test_scenario_label - tester_name - test_stop_reason

Home Screen Reconstruction Contract (Backend Read Model)

This section defines the minimum telemetry requirements for a backend service/app to reconstruct BionicLoop Home in near-real time.

Required event families (must ingest + query)

  1. CGM stream
  2. cgm.reading.processed
  3. cgm.reading.masked
  4. cgm.connection.changed
  5. cgm.state.changed

  6. Loop runtime stream

  7. loop.session.armed
  8. loop.session.reset
  9. loop.step.executed
  10. loop.step.skipped

  11. Command and delivery stream

  12. loop.command.requested
  13. loop.command.applied
  14. loop.command.blocked
  15. pump.command.result

  16. Pump status/lifecycle stream

  17. pump.status.refreshed
  18. pump.connection.changed
  19. pump.pod.lifecycle

  20. Alert lifecycle stream

  21. alert.issued
  22. alert.retracted
  23. alert.acknowledged

Home UI element -> telemetry dependencies

  1. Top CGM card (value mask behavior + stale handling)
  2. Primary source: cgm.reading.processed (value_mgdl, reading_timestamp)
  3. Mask source: cgm.reading.masked (mask_reason, latest_timestamp)
  4. Status context: cgm.state.changed, cgm.connection.changed

  5. Loop status tile (Ready/Active/Aging/No CGM/No Pod/Stale)

  6. loop.session.armed / loop.session.reset
  7. loop.step.executed / loop.step.skipped with:
  8. expected_step, executed_step, skip_reason, used_unavailable_pump_status
  9. pump.status.refreshed (delivery_state, has_active_pod, has_established_session)

  10. Pod card (reservoir text/fill/tone)

  11. pump.status.refreshed (reservoir_level_u, delivery_state, has_active_pod)
  12. pump.pod.lifecycle for replacement/deactivation transitions
  13. pump.connection.changed for disconnected state transitions

  14. Combined chart (CGM line + insulin bars)

  15. CGM line: cgm.reading.processed sequence by reading_timestamp
  16. Insulin bars (delivered and meal-highlight eligibility):
  17. loop.step.executed with step/output context
  18. pump.command.result and loop.command.applied for delivered/requested reconciliation
  19. keep zero-dose steps (for timeline parity)

  20. Alert carousel/center

  21. alert.issued, alert.retracted, alert.acknowledged
  22. use dedupe_key as primary UI identity key

Minimum read-model projections required

  1. home_latest_state_by_subject
  2. one row/document per (subject_id, auth_user_sub, app_env) with:
  3. latest CGM display state
  4. latest pump status
  5. loop session status + latest cadence signals
  6. active alerts

  7. home_cgm_timeseries

  8. append-only (or idempotent-upsert by (subject_id, reading_timestamp)) for chart windows (4/8/12/24h)

  9. home_insulin_step_timeseries

  10. step-level timeline keyed by (subject_id, executed_step or event_id):
  11. requested/delivered units
  12. wake cause
  13. step timestamp
  14. meal-associated marker when available from loop snapshots

  15. home_alert_state

  16. active alerts keyed by dedupe_key
  17. cleared history with cleared_at

Field-level requirements for Home parity

  1. Identity/correlation
  2. envelope fields mandatory: subject_id, auth_user_sub, event_id, session_id, created_at, app_env

  3. Time semantics

  4. all timeline rendering and freshness logic must use UTC-normalized timestamps from envelope/event payload

  5. Idempotency

  6. dedupe event ingest by (subject_id, event_id)
  7. dedupe alert state by dedupe_key

Known parity gaps (current app emitters)

  1. App-side ui.critical.* canonical control coverage is complete (J3); backend timeline projections should now ingest and preserve all canonical element_id paths.

  2. Loop step payload includes step_executed_at; backend should prefer it over envelope created_at for execution-time semantics.

Acceptance criteria (Home reconstruction)

Backend implementation is considered complete when: 1. For a given subject_id, backend Home read model matches app Home state (CGM card, loop tile, pod tile, charts, active alerts) under normal operation and reconnect/error transitions. 2. Masked CGM states (--) are reproduced when cgm.reading.masked indicates unreliable/stale inputs. 3. Loop status transitions are reproducible from cadence events and skip reasons. 4. Insulin chart reflects reconciled delivered doses and zero-dose steps. 5. Alert lifecycle (issue/retract/acknowledge) remains consistent across app relaunch and backend replay.

Handler Routing Requirements (BionicScout)

Implement router by event_type with strict validation:

  1. Envelope validator
  2. validate required envelope fields and value formats
  3. reject invalid with 400 + structured error body

  4. Event schema validator

  5. select schema by (event_type, schema_version)
  6. reject unknown event_type or unsupported schema_version with 400

  7. Idempotent write

  8. upsert on (subject_id, event_id)

  9. Storage fanout

  10. raw ingest table/store
  11. normalized timeline table/store
  12. optional stream routing for analytics

  13. app.log.batch fanout

  14. send entries to CloudWatch log group partitioned by app_env
  15. preserve correlation metadata (subject_id, session_id, event_id)

Response Contract

Success: - HTTP 202 Accepted - body includes: - status: "accepted" - ingest_id - deduped: true|false

Client errors: - HTTP 400 for validation/schema failures - HTTP 401/403 for auth/scope failures

Server errors: - HTTP 5xx; caller retries via app outbox policy

Observability for Backend Handlers

Emit backend metrics: - ingest request count by event_type - validation failure count by rule - dedupe hit rate - per-event latency - persistence failures

Emit alarms: - sustained 5xx - DLQ growth - schema failure spikes

Redaction and Data Minimization

  • Do not persist credentials/tokens in payload.
  • Treat free-text fields as potentially sensitive; enforce allowlist for app.log.batch.metadata keys.
  • Keep raw envelopes access-controlled by role.

Acceptance Test Set for BionicScout (must pass)

  1. Accept valid event envelope and return 202.
  2. Reject missing required envelope fields with 400.
  3. Reject unsupported schema_version with 400.
  4. Enforce scope gate (bionicscout.dev.api/telemetry.ingest).
  5. Idempotent replay of same (subject_id, event_id) does not duplicate persisted records.
  6. Validate each family sample payload and route to expected sink.
  7. app.log.batch fanout sends each entry to CloudWatch with required correlation metadata.

Current App Implementation Notes (for backend sequencing)

Implemented app-side: - required envelope fields + correlation fields - persistent outbox with retry/backoff and permanent-failure classification - runtime/CGM/pump/alert event emitters - log-level policy + app.log.batch emission path

Remaining app-side expansion: - optional additional Algo2015 normalization beyond current step_hint extraction

Backend should still implement full handler contract now so these new event families can land without API redesign.