Skip to content

[BUG FIX] [MER-5885] Restore UpGrade data capture parity for native A/B experiments - #6789

Open
eliknebel wants to merge 19 commits into
hotfix-v0.34.2from
MER-5885-restore-up-grade-data-capture-parity-for-native-a-b-experiments
Open

[BUG FIX] [MER-5885] Restore UpGrade data capture parity for native A/B experiments#6789
eliknebel wants to merge 19 commits into
hotfix-v0.34.2from
MER-5885-restore-up-grade-data-capture-parity-for-native-a-b-experiments

Conversation

@eliknebel

@eliknebel eliknebel commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

https://eliterate.atlassian.net/browse/MER-5885

Prerequisite work: This PR is based on PR #6786 and PR #6784. Both prerequisites have been squash-merged into hotfix-v0.34.1. This branch has been rebuilt on those squash commits, the superseded prerequisite commits were removed, and the complete MER-5885 diff was reverified.

Summary

Restores the experiment analytics data needed for UpGrade-style research and analysis while keeping raw learner outcomes independent from optional causal attribution.

Experiment evidence

  • restores focused experiment attribution for evaluated part, activity, and page attempts
  • restores attribution for media statements using authoritative attempt or published-section content
  • preserves weighted-random assignment scopes and existing Thompson Sampling outcome, reward, and policy-update semantics
  • carries complete experiment, assignment, intervention, condition, enrollment, project, publication, algorithm, policy-version, revision, and attempt identities where applicable
  • fails safely when assignment or selected-branch evidence cannot be established, without suppressing the underlying xAPI statement
  • keeps explicit experiment rewards separate from attempt scores; missing reward_value remains NULL, while explicit values including 0.0 are preserved

Initial condition assignments

  • adds a dedicated experiment_condition_assigned xAPI statement and raw-event type
  • emits exactly once when a new assignment is persisted and does not emit again for sticky reuse
  • records the exact persisted assigned_at value and links the attribution to the dedicated statement hash
  • supports weighted-random and Thompson Sampling assignments across intervention and section-enrollment scopes

Raw analytics and ClickHouse parity

  • propagates pseudonymous enrollment_id across applicable attempt, page-view, tutor-message, and video statements
  • preserves Torus account IDs in raw_events.user_id without falling back to actor email/mbox values
  • adds the nullable raw-event and experiment-attribution fields required for analysis and historical replay
  • normalizes projection behavior across direct upload, Lambda transformation, and replay/backfill
  • renames the attribution relationship column to raw_event_type for clarity before production release
  • consolidates the unreleased ClickHouse experiment migrations into the original migration, consistent with the planned QA database reset
  • adds a deterministic, project-scoped ClickHouse query that reconstructs the UpGrade v0.33.0 compatibility dataset from raw outcomes and assignment evidence

Researcher and operator experience

  • adds researcher documentation covering experiment workflow, terminology, S3 xAPI shape, ClickHouse fields, stable keys, event/attribution relationships, privacy boundaries, and example analysis queries
  • displays the experiment UUID in experiment details and improves the responsive, accessible details layout
  • constrains ClickHouse confirmation dialogs to the established modal sizing pattern

Important design boundaries

  • PostgreSQL remains the runtime source of truth; S3 xAPI remains the replay source; ClickHouse remains an analytical projection
  • raw evaluated outcomes are captured whether or not experiment attribution succeeds
  • condition-assignment delivery remains best-effort; a transactional outbox, reconciliation, and historical assignment backfill are deferred
  • exact attempt-time publication provenance remains the separately tracked MER-5889 limitation
  • S3 xAPI may contain sensitive learner information; ClickHouse projections use bounded identifiers, including linkable pseudonymous enrollment IDs, and remain subject to approved analytics access controls

Verification

  • 239 affected Elixir/LiveView tests passed, with 1 excluded
  • 26 Lambda ETL tests passed
  • mix compile
  • mix format --check-formatted
  • git diff --check
  • requirements traceability passed for FDD, plan, and implementation evidence
  • harness work-item validation passed
  • security, performance, Elixir, UI, TypeScript, and requirements reviews completed with no remaining findings

Reconciliation status

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️ PR is large (6515 LOC changed). Consider splitting.

Risk score: 24 → risk/high

Generated by 🚫 dangerJS against 59c9003

@github-actions

Copy link
Copy Markdown
Contributor

AI Review — elixir

No issues found

@github-actions

Copy link
Copy Markdown
Contributor

AI Review — performance

No issues found

@github-actions

Copy link
Copy Markdown
Contributor

AI Review — ui

No issues found

@github-actions

Copy link
Copy Markdown
Contributor

AI Review — typescript

No issues found

@github-actions

Copy link
Copy Markdown
Contributor

AI Review — security

No issues found

@eliknebel
eliknebel force-pushed the MER-5885-restore-up-grade-data-capture-parity-for-native-a-b-experiments branch from e019915 to d66de5f Compare August 20, 2026 19:38
@eliknebel
eliknebel marked this pull request as ready for review August 20, 2026 19:47
@nicocirio
nicocirio self-requested a review August 21, 2026 14:13
@eliknebel
eliknebel force-pushed the hotfix-v0.34.1 branch 2 times, most recently from 99219bd to 2854b3b Compare August 21, 2026 17:11
@eliknebel
eliknebel changed the base branch from hotfix-v0.34.1 to hotfix-v0.34.2 August 21, 2026 17:30
@eliknebel
eliknebel force-pushed the MER-5885-restore-up-grade-data-capture-parity-for-native-a-b-experiments branch from d66de5f to 59c9003 Compare August 21, 2026 17:36
FROM raw_events
WHERE section_id = {section_id:UInt64}
AND event_type = 'activity_attempt'
AND verb_id = 'http://adlnet.gov/expapi/verbs/evaluated'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this query cannot currently reconstruct the compatibility dataset from the activity events we emit. It filters verb_id = '.../evaluated', but ActivityAttemptEvaluated.new/3 emits .../completed and both direct/Lambda project that as activity_attempt.

The producer test currently asserts the enrollment extension but not the emitted verb, while the compatibility-query test uses hand-built raw-event fixtures whose default verb is .../evaluated. Could we make these paths share one contract—align the query with the emitted verb and build the compatibility fixture from ActivityAttemptEvaluated.new/3 (or assert the same canonical verb in both tests)—so this cannot drift again?

) AS raw
ASOF LEFT JOIN
(
SELECT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, a section may have active experiments at different Decision Points. This compatibility query only takes section_id as scope, unlike the query cookbook (docs/user/experiments/queries.md), which scopes experiment analysis by experiment_id. Could we either add an experiment_id parameter here or explicitly document that the query picks the most recent assignment for an enrollment regardless of experiment? Otherwise a researcher could silently attach a condition from a different concurrent experiment to an outcome.

end)
end

defp hydrate_assignment_match(match) do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hydrate_assignment_match/1 here is nearly identical to the one in media_attributions.ex, apart from section_slug. Would it make sense to extract the common hydration step so the two projections cannot drift the next time their selected fields change?

)

result
case result do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small consistency note, not a bug: the single-assignment path receives {:ok, decision} for sticky reuse and therefore never calls its emission callback, while the batch path reaches ConditionAssignmentEmitter for the same case and relies on its reused?: true clause to return :ok. Both paths are covered and correct today. Would it be worth converging on one place to own that guard when this flow is next touched?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants