Analytics: Environment Filtering — CAEM: accept `environment` at ingest and filter on it in the query API
@jcastro-dotcms is already working on this.
Since Sep 8, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Summary
CAEM (Content Analytics Event Manager) accepts an optional environment parameter at ingest and honors it as a filter on the three live query endpoints — GET /v1/events, GET /v1/content, GET /v1/sessions — following the exact pattern already used for project. Omitted → today's unfiltered, blended-across-environments behavior, unchanged.
Parent epic: #37349
Spike: #37107
Depends on: the ClickHouse schema sub-task (recreates analytics.events + the 10 downstream tables with environment as a key dimension) — nothing here can write to or filter on a column that doesn't exist yet.
Background
Per ADR-0022 rule 2, both changes below are the safe, directly-shippable case: "new optional query parameter... default must be exactly what the endpoint already did before the parameter existed."
- Ingest (
POST /v1/event/ingest): today acceptstenant(from the Bearer token) andproject(request param) only. Addenvironment, threaded the same wayprojectalready is:IngestionContext→SaveEventRequest→SaveEventRepository.insertChunk(). Omitted →environment = ''(today's implicit, fully-commingled behavior). - Query endpoints:
EventsAnalyticsController,ContentAnalyticsController,SessionsAnalyticsControllereach get an optionalenvironment@RequestParam, per the standard controller-validation conventions (.claude/conventions.md) — no@Size, no@Pattern(it's an open string set sourced from infra config, not a fixed enum, same reasoning as whyprojecthas no pattern constraint either). Omitted → no filter, preserving compatibility for any caller that hasn't adopted the parameter yet.
No EventAnalyticsProxyHelper (dotCMS-core) change needed for the query side — verified in the spike: it forwards every query parameter from the original request transparently (uriInfo.getQueryParameters().forEach(...)) before appending project= only if absent, and events/sessions/content are already in its ALLOWED_PATH_PREFIXES. Once Angular adds &environment=prod to a call, it reaches CAEM with zero core-proxy code changes. (The ingest side is different — the collector has to be told its own environment value to send it; that's covered by the dotCMS-core sub-task, not this one.)
The older, @Deprecated(forRemoval = true) per-metric controllers get no new work.
Scope
POST /v1/event/ingest: optionalenvironmentrequest param, same shape/threading asproject, throughIngestionContext→SaveEventRequest→SaveEventRepository.insertChunk().GET /v1/events,GET /v1/content,GET /v1/sessions: optionalenvironment@RequestParam, filtering the underlying QueryDSL predicates against the now-environment-aware tables.- Update the hand-written QueryDSL Q-types (
QContentEventsCounter,QPageviewsByDeviceBrowserDaily,QContentPresentsInConversion,QEngagementDaily,QSessionsByDeviceDaily,QSessionsByBrowserDaily,QSessionsByLanguageDaily) to expose anenvironmentfield, mirroring how each already exposestenant/project— moved here from #37406, which is schema-only (docker/init/*.sql). Matches this codebase's own precedent: the experiment columns added by #37016/#37017 never touched a single Q-type until a query actually needed to filter/select by them; a Q-type gains a field only when the query work that uses it lands, not when the underlying column is created. - Update the shared predicate builders (
QueryPredicateBuilder,EventQueryPredicateBuilder,AttributionQueryPredicateBuilder,AnalyticsQueryPredicateBuilder) andAnalyticsQueryto carryenvironmentalongsidetenant/project/siteId. - Update
.claude/conventions.md's controller-parameter table to documentenvironmentalongside the existing rows. - Per
.claude/testing.md's Postman maintenance rule: updatepostman/CAEM-dotCMS-Proxy.postman_collection.json— addenvironmentto relevant POST bodies and GET query strings, plus a negative/empty-environment case. - Update the IT suites per the existing checklist:
EventIngestionControllerIT,EventsAnalyticsControllerIT,ContentAnalyticsControllerIT,SessionsAnalyticsControllerIT(happy path withenvironmentset, omitted-param backward-compat case, cross-environment isolation case).
Out of scope
EventAnalyticsProxyHelperchanges — confirmed unnecessary (see Background).- The deprecated per-metric controllers.
- Authorization for cross-environment reads — explicitly deferred by the epic; CAEM auth stays tenant-scoped, nothing here restricts which environments a valid Bearer token can query.
- ClickHouse schema/table changes (previous sub-task, #37406) — the column and its
ORDER BY/PARTITION BYposition are already defined there; this ticket only adds the Java-side Q-type fields and query-layer support.
Dependencies
Blocked on the ClickHouse schema sub-task landing first. The dotCMS-core sub-task (collector plumbing) and this sub-task can proceed in parallel — an unrecognized request param is silently ignored by Spring MVC, and an omitted param defaults safely on both sides, so there's no required ordering between "collector sends &environment=" and "CAEM reads it."
Acceptance Criteria
- Ingesting an event with
environmentset persists it correctly onanalytics.events; omitting it persists''(unchanged from today) - All three query endpoints accept optional
environmentand return only that environment's data when set - All 7 Q-types (
QContentEventsCounter,QPageviewsByDeviceBrowserDaily,QContentPresentsInConversion,QEngagementDaily,QSessionsByDeviceDaily,QSessionsByBrowserDaily,QSessionsByLanguageDaily) exposeenvironmentand compile - Omitting
environmenton any query endpoint reproduces today's unfiltered behavior exactly (epic acceptance criterion) - Cross-environment isolation verified end-to-end (ingest into two environments for the same tenant/project, query one, confirm the other's rows are excluded)
- Postman collection updated per
.claude/testing.md's maintenance rule - All new/changed IT classes pass under
mvn verify
Next step
Run /speckit-specify against this issue to produce the formal spec before implementation.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.