Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ Depends: R (>= 4.1.0)
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Suggests:
Suggests:
testthat (>= 3.0.0),
withr
withr,
yaml
Config/testthat/edition: 3
Imports:
cli,
Expand Down
83 changes: 83 additions & 0 deletions R/registry.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Registry lifecycle state
#
# The canonical field registry lives in cvr-cortex
# (features/slicer/slicer_fields.yaml). It marks two orthogonal facts:
#
# deprecated: consumers should stop reading the key (a replacement is
# usually named in the entry description). Says nothing about
# production — several deprecated keys are still emitted.
# sunsetted: no shipping SDK emits the key. Historical data may still be
# queryable.
#
# This package has no schema-sync machinery (unlike cognitive3dpy, which
# generates its copy), so the lists below are maintained by hand. Keep them
# in sync with the registry; test-compact-columns.R cross-checks them against
# the YAML whenever a cvr-cortex checkout sits alongside this repo.
#
# Last synced against slicer_fields.yaml: 2026-08-12

# Raw registry keys marked "deprecated: true".
deprecated_registry_keys <- c(
"c3d.device.eyetracking.type",
"c3d.height",
"c3d.metric_components.forward_reach_score",
"c3d.metric_components.fps_data_point_count",
"c3d.metric_components.fps_data_point_sum",
"c3d.metric_components.horizontal_reach_score",
"c3d.metric_components.pitch_score",
"c3d.metric_components.roll_score",
"c3d.metric_components.vertical_reach_score",
"c3d.metrics.app_performance",
"c3d.metrics.battery_efficiency",
"c3d.metrics.boundary_score",
"c3d.metrics.controller_engagement_score",
"c3d.metrics.controller_ergonomic_score",
"c3d.metrics.controller_events_score",
"c3d.metrics.dynamic_engagement_score",
"c3d.metrics.ergonomics_score",
"c3d.metrics.head_orientation_score",
"c3d.metrics.immersion_score",
"c3d.metrics.orientation_score",
"c3d.metrics.standing_percentage",
"c3d.participant.hmdHeight",
"c3d.roomsize"
)

# Raw registry keys marked "sunsetted: true".
sunsetted_registry_keys <- c(
"c3d.app.androidPlugin.hostName",
"c3d.app.androidPlugin.networkHostName",
"c3d.app.multiplayer.lobbyId",
"c3d.app.plugin.version",
"c3d.device.manufacturer",
"c3d.device.screenresolution",
"c3d.device.serial_number",
"c3d.device.serialnumber",
"c3d.headphonespresent",
"c3d.height",
"c3d.oculusId",
"c3d.participant.Age",
"c3d.participant.Color",
"c3d.participant.Job",
"c3d.participant.Sex",
"c3d.participant.hmdHeight",
"c3d.roomscale",
"cvr.device.graphics.memory",
"cvr.device.graphics.version",
"cvr.device.platform",
"cvr.vr.display.family",
"cvr.vr.display.model",
"cvr.vr.enabled"
)

# The same keys as cleaned column names, matching what the parsing pipeline
# produces.
deprecated_registry_columns <- function() {
vapply(deprecated_registry_keys, clean_property_name, character(1),
USE.NAMES = FALSE)
}

sunsetted_registry_columns <- function() {
vapply(sunsetted_registry_keys, clean_property_name, character(1),
USE.NAMES = FALSE)
}
21 changes: 13 additions & 8 deletions R/sessions.R
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ to_epoch_ms <- function(x) {
# --- Session response parsing ---

# Column list for compact mode (~40 columns)
#
# Curated by hand — the registry does not carry per-package presentation
# intent. It does carry lifecycle state, and test-compact-columns.R fails if
# anything here is deprecated or sunsetted in the registry (see R/registry.R).
# Data dropped from this list is still returned by compact = FALSE.
compact_columns <- c(
# Core session fields (top-level API fields)
"session_id", "session_date", "end_date", "duration_s", "hmd", "device_id",
Expand All @@ -398,14 +403,12 @@ compact_columns <- c(
"c3d_geo_country", "c3d_geo_subdivision", "c3d_geo_city",
"c3d_roomsize_meters",
# C3D properties: key metrics (top-level scores)
"c3d_metrics_fps_score", "c3d_metrics_app_performance",
"c3d_metrics_average_fps", "c3d_metrics_presence_score",
"c3d_metrics_immersion_score", "c3d_metrics_orientation_score",
"c3d_metrics_comfort_score", "c3d_metrics_ergonomics_score",
"c3d_metrics_battery_efficiency", "c3d_metrics_boundary_score",
"c3d_metrics_controller_events_score",
"c3d_metrics_controller_engagement_score",
"c3d_metrics_dynamic_engagement_score", "c3d_metrics_standing_percentage",
# c3d_metrics_average_fps: melder intends to delete the emission but the org
# dashboard tile still reads it, so the registry keeps it active. See
# cvr-cortex investigations/average-fps-intended-deletion-vs-live-consumer.md
# before removing it.
"c3d_metrics_fps_score", "c3d_metrics_average_fps",
"c3d_metrics_presence_score", "c3d_metrics_comfort_score",
# C3D properties: metric components (sub-scores)
"c3d_metric_components_fps_score_degree_app_performance",
"c3d_metric_components_fps_score_consistency_app_performance",
Expand All @@ -422,6 +425,8 @@ compact_columns <- c(
"c3d_metric_components_presence_score_interruption_score",
"c3d_metric_components_presence_score_controller_movement_score",
"c3d_metric_components_cyberwellness_visual_continuity",
# Replaces the retired c3d.metrics.standing_percentage compute
"c3d_metric_components_posture_standing_percentage",
# C3D properties: session flags
"c3d_session_tag_junk", "c3d_session_tag_test"
)
Expand Down
90 changes: 90 additions & 0 deletions tests/testthat/test-compact-columns.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Guard compact_columns against drift from the cortex field registry.
#
# The list is curated by hand, so it can go stale in two ways: a column can
# name a key the registry has retired, or the hand-maintained lifecycle lists
# in R/registry.R can themselves fall behind the registry. The first three
# tests always run; the last one runs only when a cvr-cortex checkout sits
# alongside this repo.

# Registry-retired columns deliberately kept in compact output.
# Name = column, value = why it stays. Empty today.
lifecycle_exceptions <- character(0)

cortex_yaml_path <- function() {
candidates <- c(
Sys.getenv("SLICER_YAML", unset = NA_character_),
file.path("..", "..", "..", "cvr-cortex", "features", "slicer",
"slicer_fields.yaml")
)
candidates <- candidates[!is.na(candidates)]
hit <- candidates[file.exists(candidates)]
if (length(hit) == 0) NULL else normalizePath(hit[[1]])
}

test_that("compact_columns has no duplicates", {
expect_equal(anyDuplicated(compact_columns), 0)
})

test_that("no registry-deprecated columns in compact output", {
offenders <- setdiff(
intersect(compact_columns, deprecated_registry_columns()),
lifecycle_exceptions
)
expect(
length(offenders) == 0,
paste(
"Deprecated in the registry:", paste(offenders, collapse = ", "),
"- drop them from compact_columns (the data stays available via",
"compact = FALSE), or add them to lifecycle_exceptions with a reason."
)
)
})

test_that("no registry-sunsetted columns in compact output", {
offenders <- setdiff(
intersect(compact_columns, sunsetted_registry_columns()),
lifecycle_exceptions
)
expect(
length(offenders) == 0,
paste(
"Sunsetted in the registry:", paste(offenders, collapse = ", "),
"- drop them, or add them to lifecycle_exceptions with a reason."
)
)
})

test_that("lifecycle lists match the cortex registry", {
skip_if_not_installed("yaml")
yaml_path <- cortex_yaml_path()
skip_if(is.null(yaml_path), "cvr-cortex checkout not found")

registry <- yaml::read_yaml(yaml_path)

flagged <- function(section, grouped, flag) {
entries <- registry[[section]]
if (is.null(entries)) return(character(0))
if (grouped) entries <- unlist(unname(entries), recursive = FALSE)
keys <- names(entries)
if (is.null(keys)) return(character(0))
keys[vapply(entries, function(meta) isTRUE(meta[[flag]]), logical(1))]
}

collect <- function(flag) {
sort(unique(c(
flagged("session_fields", FALSE, flag),
flagged("event_fields", FALSE, flag),
flagged("session_properties", TRUE, flag),
flagged("event_properties", TRUE, flag)
)))
}

expect_equal(
sort(deprecated_registry_keys), collect("deprecated"),
info = "R/registry.R deprecated_registry_keys is stale — resync it."
)
expect_equal(
sort(sunsetted_registry_keys), collect("sunsetted"),
info = "R/registry.R sunsetted_registry_keys is stale — resync it."
)
})
21 changes: 20 additions & 1 deletion tests/testthat/test-sessions.R
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,26 @@ test_that("participant_id is parsed", {
test_that("comfort and engagement scores are parsed", {
result <- parse_sessions(fixture$results, compact = TRUE)
expect_equal(result$c3d_metrics_comfort_score[1], 74.34505247920842)
expect_equal(result$c3d_metrics_controller_engagement_score[1], 54.27067375518957)
# controller_engagement_score is deprecated in the registry, so it is no
# longer curated into compact output — still parsed in full mode.
full_result <- parse_sessions(fixture$results, compact = FALSE)
expect_equal(
full_result$c3d_metrics_controller_engagement_score[1],
54.27067375518957
)
})

test_that("columns dropped from compact are still returned in full mode", {
full_result <- parse_sessions(fixture$results, compact = FALSE)
for (column in c(
"c3d_metrics_app_performance",
"c3d_metrics_immersion_score",
"c3d_metrics_battery_efficiency",
"c3d_metrics_standing_percentage"
)) {
expect_false(column %in% compact_columns)
expect_true(column %in% names(full_result))
}
})

test_that("parse_sessions() handles empty input", {
Expand Down
Loading