Description
Part 1 of #36763 — prove that an experiment block set by a browser survives dotCMS's analytics proxy and lands in ClickHouse.
This is the day-one unblocker for the whole sprint. Everything else in Part 1's first sprint (schema, ingest) can only be verified with hand-seeded data unless this path is confirmed, because nothing sets context.experiment until the SDK work in Part 3 and that developer is unavailable.
The gap
Experiment events reach the analytics pipeline through core, not directly:
browser SDK → dotCMS core → CAEM → ClickHouse
↑
POST /api/v1/analytics/content/event
EventAnalyticsProxyResource
The implementation plan covers the CAEM end (UserEventPayload, SaveEventRepository) and the ClickHouse end (schema), but silently assumes the middle hop passes context.experiment through untouched. That assumption is never stated, and it gates every end-to-end assertion in the sprint.
Why it should pass — and why it still needs a test
Reading EventAnalyticsProxyResource.proxyEventRequest, the handler parses the body, validates context.site_auth via SiteAuthValidator, injects site_id from the resolved site, re-serializes, and forwards to {DOT_ANALYTICS_BASE_URL}/v1/event/ingest. It does not run AnalyticsValidatorUtil. So an unrecognised experiment key in context should survive.
"Should" is doing real work in that sentence. A round-trip test converts it into a fact, and leaves behind the fixture the sprint needs as its only real producer.
⚠ The trap this test also documents
The legacy ingest path behaves differently. /api/v1/event → EventLogWebInterceptor validates against dotCMS/src/main/resources/analytics/validators/all.json, which is an allowlist on context — site_auth, session_id, user_id, device and nothing else. An unknown key produces ValidationErrorCode.UNKNOWN_FIELD and a 400.
So any interim producer routed through /api/v1/event needs all.json updated first. The implementation plan's note that "JSON schema validators require no changes" is true for CAEM's validators and not true for core's legacy path. Recording this here so the next person does not lose a day to it.
Acceptance Criteria
Round-trip
Negative and edge
Documentation
Priority
High
Additional Context
Do this on day one, before the schema work is finished. If the assumption turns out to be wrong, the fix is small but it changes what the schema and ingest issues can claim as "verified", and it is much cheaper to discover in week one than at sprint review.
Open question this test informs. If a synthetic fixture is not enough, the alternative interim producer is having dotCMS inject context.experiment server-side at the proxy — it already resolves the assigned variant via isUserIncluded and the variantName parameter. That would give real end-to-end experiment data with no SDK developer, but it adds core work to this sprint and changes what Part 3 has to build. Flagging as a scoping decision, not proposing it here.
Related: the schema and ingest issues in this sprint depend on this for anything beyond hand-seeded verification.
Parent epic: #36763
Description
Part 1 of #36763 — prove that an
experimentblock set by a browser survives dotCMS's analytics proxy and lands in ClickHouse.This is the day-one unblocker for the whole sprint. Everything else in Part 1's first sprint (schema, ingest) can only be verified with hand-seeded data unless this path is confirmed, because nothing sets
context.experimentuntil the SDK work in Part 3 and that developer is unavailable.The gap
Experiment events reach the analytics pipeline through core, not directly:
The implementation plan covers the CAEM end (
UserEventPayload,SaveEventRepository) and the ClickHouse end (schema), but silently assumes the middle hop passescontext.experimentthrough untouched. That assumption is never stated, and it gates every end-to-end assertion in the sprint.Why it should pass — and why it still needs a test
Reading
EventAnalyticsProxyResource.proxyEventRequest, the handler parses the body, validatescontext.site_authviaSiteAuthValidator, injectssite_idfrom the resolved site, re-serializes, and forwards to{DOT_ANALYTICS_BASE_URL}/v1/event/ingest. It does not runAnalyticsValidatorUtil. So an unrecognisedexperimentkey incontextshould survive."Should" is doing real work in that sentence. A round-trip test converts it into a fact, and leaves behind the fixture the sprint needs as its only real producer.
⚠ The trap this test also documents
The legacy ingest path behaves differently.
/api/v1/event→EventLogWebInterceptorvalidates againstdotCMS/src/main/resources/analytics/validators/all.json, which is an allowlist oncontext—site_auth,session_id,user_id,deviceand nothing else. An unknown key producesValidationErrorCode.UNKNOWN_FIELDand a 400.So any interim producer routed through
/api/v1/eventneedsall.jsonupdated first. The implementation plan's note that "JSON schema validators require no changes" is true for CAEM's validators and not true for core's legacy path. Recording this here so the next person does not lose a day to it.Acceptance Criteria
Round-trip
dotcms-postmanPOSTs a payload containingcontext.experiment(id,running_id,variant) toPOST /api/v1/analytics/content/eventand asserts a success responseexperimentblock is confirmed to arrive at CAEM unmodified — keys and values intact, nothing dropped or renamedanalytics.eventsfor the ingested eventsite_idinjection still works with theexperimentblock present — the block does not interfere with the existing proxy behaviourNegative and edge
experimentblock still succeeds and is unchanged from today's behaviour — no regression to non-experiment analyticsexperimentblock is rejected with 400 by CAEM's Bean Validation, and the error surfaces through the proxy in a usable form rather than as an opaque 500Documentation
contextthrough; the legacy/api/v1/eventpath allowlists it viaall.jsonPriority
High
Additional Context
Do this on day one, before the schema work is finished. If the assumption turns out to be wrong, the fix is small but it changes what the schema and ingest issues can claim as "verified", and it is much cheaper to discover in week one than at sprint review.
Open question this test informs. If a synthetic fixture is not enough, the alternative interim producer is having dotCMS inject
context.experimentserver-side at the proxy — it already resolves the assigned variant viaisUserIncludedand thevariantNameparameter. That would give real end-to-end experiment data with no SDK developer, but it adds core work to this sprint and changes what Part 3 has to build. Flagging as a scoping decision, not proposing it here.Related: the schema and ingest issues in this sprint depend on this for anything beyond hand-seeded verification.
Parent epic: #36763