feat: Support Experiments (A/B tests) on Swetrix CE#528
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (9)
📝 WalkthroughWalkthroughAdds a full A/B experimentation feature: data model and ClickHouse tables, REST APIs and DTOs, deterministic variant assignment, Bayesian result computation, feature-flag integration, exposure tracking for evaluations and custom events, module wiring, migrations, and UI tab visibility for Experiments. ChangesA/B Experimentation System
Sequence DiagramsequenceDiagram
participant Client
participant ExperimentController
participant ExperimentService
participant ClickHouse as ClickHouse (events, experiment, experiment_exposures)
participant Bayesian as calculateBayesianProbabilities
Client->>ExperimentController: GET /experiment/:id/results
ExperimentController->>ExperimentService: findOneWithRelations(id)
ExperimentService->>ClickHouse: SELECT experiment, variants
ClickHouse-->>ExperimentService: experiment + variants
ExperimentService-->>ExperimentController: Experiment{variants}
ExperimentController->>ClickHouse: Query exposures (experiment_exposures)
ClickHouse-->>ExperimentController: exposures per variant
alt goal exists
ExperimentController->>ClickHouse: Query conversions (events)
ClickHouse-->>ExperimentController: conversions per variant
end
ExperimentController->>Bayesian: calculateBayesianProbabilities(variants)
Bayesian->>Bayesian: seeded Monte Carlo Beta sampling
Bayesian-->>ExperimentController: variant win probabilities
ExperimentController-->>Client: ExperimentResultsDto (variants, winner, chart?)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (9)
backend/apps/community/src/experiment/bayesian.ts (1)
40-72: 💤 Low valueOptional: dead branch for
shape < 1.Given that
calculateBayesianProbabilitiesalways callssampleGammawithalpha = conversions + 1 ≥ 1andbeta = Math.max(1, ...) ≥ 1, the recursive boost branch on lines 69–71 is unreachable in practice. You can either drop it for clarity or keep it as defensive future-proofing in case the function is reused withshape < 1callers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/experiment/bayesian.ts` around lines 40 - 72, The recursive branch handling shape < 1 in sampleGamma is effectively dead because calculateBayesianProbabilities only calls sampleGamma with alpha and beta >= 1; remove the lower-than-1 branch (the recursive call and subsequent acceptance using Math.pow(u, 1 / shape)) and either add a short guard/assert at the top of sampleGamma (e.g., throw or console.assert if shape < 1) or a comment stating the function expects shape >= 1; keep references to sampleGamma and calculateBayesianProbabilities so readers can see the invariant.backend/apps/community/src/experiment/experiment.module.ts (1)
10-22: ⚖️ Poor tradeoffLGTM, with a note on the cycle density.
Four of five imports use
forwardRef, indicating the experiment/analytics/feature-flag/goal/project quartet has dense bidirectional dependencies. This works, but if the dependency graph keeps growing it may be worth extracting a small "shared types/utilities" module (e.g.,ExperimentVariantSelectionService) to break some cycles. Not blocking for this PR.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/experiment/experiment.module.ts` around lines 10 - 22, The module currently relies on forwardRef for ProjectModule, AnalyticsModule, GoalModule and FeatureFlagModule indicating cyclic dependencies; break the cycle by extracting the shared logic into a small module (e.g., create ExperimentVariantSelectionService inside a new ExperimentSharedModule), move the variant selection/util functions from ExperimentService into that service, export ExperimentVariantSelectionService from ExperimentSharedModule and have ExperimentModule, AnalyticsModule, GoalModule, FeatureFlagModule and ProjectModule import ExperimentSharedModule instead of referencing each other via forwardRef; update ExperimentModule to remove forwardRef imports where possible and inject ExperimentVariantSelectionService into ExperimentService (and any other modules that need it) to eliminate the dense bidirectional dependency graph.backend/apps/community/src/feature-flag/feature-flag.controller.ts (3)
316-348: 💤 Low valueVariant assignment flow looks correct, minor consistency note.
Variants from
experimentServiceare already ordered bykey ASCin ClickHouse (ingetVariantsForExperiments), and you re-sort here withlocaleCompare. For ASCII-only variant keys (the typical case) both orderings match, butlocaleCompareis locale-sensitive while ClickHouse uses byte order. SincegetExperimentVariantis deterministic for any fixed input order, this is fine — but it'd be slightly cleaner to rely on the service-side ordering and drop the re-sort, or lock the ordering down to one canonical form so future refactors don't shift assignments.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/feature-flag/feature-flag.controller.ts` around lines 316 - 348, The code re-sorts experiment.variants with localeCompare before calling getExperimentVariant which duplicates and can introduce locale-dependent ordering differences versus the service-side (getVariantsForExperiments) byte-ordering; remove the re-sort and pass the variants from experiments directly (or explicitly sort using a byte/ASCII comparator if you prefer) so the ordering is canonical and deterministic—update the block around experiment.variants, sortedVariants, and the getExperimentVariant call in feature-flag.controller.ts (references: experimentService.findRunningByIds, experiment.variants, getExperimentVariant).
408-439: ⚡ Quick winConsider
async_insert: 1for both inserts on this hot path.
POST /feature-flag/evaluateis a public, high-throughput endpoint, andtrackEvaluationsperforms two sequential awaited ClickHouse inserts (feature_flag_evaluationsthenexperiment_exposures). Other ingest paths in this codebase (e.g.,analytics.controller.tsexperiment_exposuresinsert at line 1243-1255 and the events insert at line 1167-1173) useclickhouse_settings: { async_insert: 1 }for similar fire-and-forget ingestion. Aligning here would reduce p99 on the evaluate path and keep ingestion behavior consistent.♻️ Suggested change
await clickhouse.insert({ table: 'feature_flag_evaluations', values, format: 'JSONEachRow', + clickhouse_settings: { async_insert: 1 }, }) @@ await clickhouse.insert({ table: 'experiment_exposures', values: experimentExposures, format: 'JSONEachRow', + clickhouse_settings: { async_insert: 1 }, })Based on learnings from past feedback that
clickhouse_settings: { async_insert: 1 }is the accepted pattern for high-throughput, fire-and-forget ingestion in this codebase.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/feature-flag/feature-flag.controller.ts` around lines 408 - 439, Add ClickHouse async insert settings to both insert calls to make these fire-and-forget like other ingest paths: when calling clickhouse.insert for table 'feature_flag_evaluations' (using variable values) and for table 'experiment_exposures' (using experimentExposures), pass clickhouse_settings: { async_insert: 1 } in the options object (keep the existing try/catch logging behavior unchanged).
360-385: 💤 Low valueConsider not mixing two key namespaces in one map.
experimentsByIdOrFlagKeyindexes the same value by both experiment id and feature-flag key. This works in practice (flag keys are unlikely to collide with UUIDs), but it makes the response shape harder to type, harder to evolve, and forces SDK consumers to know which lookup convention to use. Splitting into two records (e.g.,experimentskeyed by id,experimentsByFlagkeyed by flag key) would be unambiguous and easier to document.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/feature-flag/feature-flag.controller.ts` around lines 360 - 385, Currently the code mixes experiment IDs and flag keys in one map (experimentsByIdOrFlagKey) which is confusing; change the response shape to have two separate optional records (e.g., response.experiments: Record<string,string> keyed by experimentId and response.experimentsByFlag: Record<string,string> keyed by flag key), update the response type declaration to include both, then in the loop over experimentVariants assign experiments[experimentId] = variantKey and for each linkedFlag assign experimentsByFlag[linkedFlag.key] = variantKey (instead of writing both into one map), and finally set response.experiments and response.experimentsByFlag before returning.backend/apps/community/src/experiment/experiment.controller.ts (3)
416-491: 💤 Low valueResuming a paused experiment overwrites the original
startedAt.The start handler unconditionally sets
startedAt: dayString()(line 486), so resuming a PAUSED experiment loses the original start timestamp. Since results queries usegroupFrom/groupTofrom the request rather thanstartedAt, the data side is fine, but downstream views/reporting that show "experiment running since X" will report the resume time instead. Consider preservingstartedAtif it's already set:♻️ Suggested change
const updatedExperiment = await this.experimentService.update(id, { status: ExperimentStatus.RUNNING, - startedAt: dayString(), + ...(experiment.startedAt ? {} : { startedAt: dayString() }), featureFlagId, })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/experiment/experiment.controller.ts` around lines 416 - 491, The handler currently always sets startedAt: dayString() when calling this.experimentService.update (see variable updatedExperiment and the update call in experiment.controller.ts), which overwrites the original start time when resuming a PAUSED experiment; change the payload to preserve the existing experiment.startedAt if present (e.g. set startedAt to experiment.startedAt ?? dayString()) so startedAt is only set to now for truly new starts and not when resuming.
1063-1073: ⚡ Quick winDead branch in
variantSelector— both enum values lead to the same expression.
MultipleVariantHandlingonly hasFIRST_EXPOSUREandEXCLUDE, so the ternary'selsebranch ('any(variantKey)') is unreachable. This is misleading and brittle: if a third enum value is added, it would silently fall through to a different aggregation. Either simplify to always useargMin(current effective behavior), or — if the original intent was to useanyforEXCLUDE(sinceHAVING uniqExact(variantKey) = 1guarantees a single variant per profile, makinganycheaper) — restore that explicitly.♻️ Suggested change (simplification)
- const variantSelector = - experiment.multipleVariantHandling === - MultipleVariantHandling.FIRST_EXPOSURE || - experiment.multipleVariantHandling === MultipleVariantHandling.EXCLUDE - ? 'argMin(variantKey, tuple(created, variantKey))' - : 'any(variantKey)' + const variantSelector = + experiment.multipleVariantHandling === MultipleVariantHandling.FIRST_EXPOSURE + ? 'argMin(variantKey, tuple(created, variantKey))' + : 'any(variantKey)'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/experiment/experiment.controller.ts` around lines 1063 - 1073, The ternary in getExposureAttributionSubquery creates a dead else-branch because MultipleVariantHandling only has FIRST_EXPOSURE and EXCLUDE and both currently resolve to argMin; simplify variantSelector to always use 'argMin(variantKey, tuple(created, variantKey))' (remove the unreachable 'any(variantKey)' branch) while leaving multiVariantFilter behavior for MultipleVariantHandling.EXCLUDE ('HAVING uniqExact(variantKey) = 1') intact so the function's logic (variantSelector and multiVariantFilter) is clear and not brittle; update references in getExposureAttributionSubquery and remove the unnecessary enum branch checks.
865-872: 💤 Low valueStrict float equality on rollout sum can reject legitimate inputs.
totalPercentage !== 100will reject any decimal split that incurs floating-point drift (e.g.,33.3 + 33.3 + 33.4works, but combinations summing through values like0.1 + 0.2won't). If the API accepts non-integer rollouts, prefer an epsilon-based comparison; if the contract is integers-only, validate that explicitly per-variant.♻️ Suggested change
- const totalPercentage = _sum( - variants.map((variant) => variant.rolloutPercentage), - ) - if (totalPercentage !== 100) { + const totalPercentage = _sum( + variants.map((variant) => variant.rolloutPercentage), + ) + if (Math.abs(totalPercentage - 100) > 0.01) { throw new BadRequestException( 'Variant rollout percentages must sum to 100', ) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/experiment/experiment.controller.ts` around lines 865 - 872, The current strict equality check using _sum over variants' rolloutPercentage (variants.map(...)) can fail due to floating-point drift; update the validation in the block that computes totalPercentage to either (a) enforce per-variant integer rollouts by validating each variant.rolloutPercentage is an integer before summing, or (b) use an epsilon-based comparison (e.g., Math.abs(totalPercentage - 100) <= EPSILON) where EPSILON is a small constant (e.g., 1e-6) and include that in the BadRequestException path; reference the existing identifiers _sum, variants, rolloutPercentage and the thrown BadRequestException when making the change.backend/apps/community/src/experiment/experiment.service.ts (1)
301-329: ⚡ Quick winConsider scoping
findRunningByIdsbyprojectIdfor defense in depth.The query trusts that
experimentIdsis already project-scoped (true today: callers derive IDs from project-scoped flags). However, adding aprojectIdfilter would make the method safe regardless of caller, and prevents cross-tenant data exposure if a future caller passes mixed IDs. Same recommendation could apply tofindOne/findOneWithRelationsfor view/manage paths, though those are followed by anallowedToView/allowedToManagecheck.♻️ Suggested signature change
async findRunningByIds( experimentIds: Array<string | null>, exposureTrigger: ExposureTrigger, + projectId?: string, ): Promise<Experiment[]> { const ids = experimentIds.filter((id): id is string => Boolean(id)) if (_isEmpty(ids)) { return [] } const { data } = await clickhouse .query({ query: ` SELECT * FROM experiment WHERE id IN {ids:Array(String)} AND status = {status:String} AND exposureTrigger = {exposureTrigger:String} + ${projectId ? 'AND projectId = {projectId:FixedString(12)}' : ''} `, query_params: { ids, status: ExperimentStatus.RUNNING, exposureTrigger, + ...(projectId ? { projectId } : {}), }, })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/apps/community/src/experiment/experiment.service.ts` around lines 301 - 329, Add a projectId filter to findRunningByIds to prevent cross-tenant exposure: update the method signature (findRunningByIds) to accept a projectId (string), add a WHERE clause AND projectId = {projectId:String} to the ClickHouse query and include projectId in query_params, and ensure callers pass the caller's projectId (or derive it) when invoking findRunningByIds; similarly consider adding optional projectId filtering to findOne/findOneWithRelations (and update their callers) so those methods can be scoped by projectId as an additional defense-in-depth measure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/apps/community/src/experiment/dto/experiment.dto.ts`:
- Around line 133-141: Add a minimum-size validator to the variants arrays so
DTO validation rejects too-small experiments: add `@ArrayMinSize`(2) (or
`@ArrayMinSize`(1) if rollout-only experiments are allowed) above the variants
property in ExperimentDto (the variants: ExperimentVariantDto[] declaration) and
likewise add the same `@ArrayMinSize`(...) to UpdateExperimentDto. Also import
ArrayMinSize from class-validator and keep the existing `@ArrayMaxSize`(20),
`@IsArray`(), `@ValidateNested`({ each: true }) and `@Type`(() =>
ExperimentVariantDto) decorators.
In `@backend/apps/community/src/experiment/experiment.controller.ts`:
- Around line 678-693: In the conversionsQuery (and the other query occurrence
that uses assumeNotNull(c.profileId)), replace the unsafe
assumeNotNull(c.profileId) usage in the JOIN with an explicit null check and a
proper equality comparison: change the JOIN clause so it requires c.profileId IS
NOT NULL and then compares e.profileId = c.profileId (instead of e.profileId =
assumeNotNull(c.profileId)); update the same pattern wherever
assumeNotNull(c.profileId) appears (e.g., the other query around the
exposureAttributionSubquery usage) so nullable profileId is handled safely.
In `@backend/migrations/clickhouse/initialise_selfhosted.js`:
- Around line 97-132: The self-hosted initializer is missing the
experiment_exposures table so fresh installs fail on exposure inserts; in
initialise_selfhosted.js add a CREATE TABLE IF NOT EXISTS
${dbName}.experiment_exposures entry (matching the schema used in
initialise_database.js/cloud initializer) alongside the existing experiment and
experiment_variant CREATE statements so the experiment_exposures table is
created during clickhouse:initialise; ensure the new table uses the same
columns, ENGINE, ORDER BY and PARTITION BY as the cloud version to keep schemas
consistent.
In `@README.md`:
- Line 60: The README has inconsistent availability for Experiments: the feature
bullet "- **Experiments**: run A/B tests and experiments to optimize your site."
is written as generally available, while the Cloud vs CE matrix row for
"Experiments" marks CE as not included; pick the correct truth and make both
places match. Update either the bullet line (the "- **Experiments**" sentence)
to indicate "Cloud only" or similar if CE does not include it, or update the
Cloud vs CE matrix row to mark CE as included if Experiments is now available in
CE; ensure you change both the bullet text and the matrix cell for "Experiments"
so they are identical.
In `@web/app/pages/Project/View/ViewProject.tsx`:
- Around line 778-797: PROJECT_TABS.experiments can be undefined in self-hosted
builds, causing experimentsTab to have id: undefined and break tab logic; update
the code so the experiments tab is only added when a valid id exists or ensure
SELFHOSTED_PROJECT_TABS defines experiments. Concretely, either add an
experiments entry to SELFHOSTED_PROJECT_TABS in web/app/lib/constants/index.ts
or change ViewProject.tsx where experimentsTab is created/added (symbols:
experimentsTab, PROJECT_TABS.experiments, isSelfhosted, newTabs) to guard the
tab creation/append by checking PROJECT_TABS.experiments !== undefined (or
filter out tabs with falsy id) before building newTabs so no tab with id:
undefined is produced.
---
Nitpick comments:
In `@backend/apps/community/src/experiment/bayesian.ts`:
- Around line 40-72: The recursive branch handling shape < 1 in sampleGamma is
effectively dead because calculateBayesianProbabilities only calls sampleGamma
with alpha and beta >= 1; remove the lower-than-1 branch (the recursive call and
subsequent acceptance using Math.pow(u, 1 / shape)) and either add a short
guard/assert at the top of sampleGamma (e.g., throw or console.assert if shape <
1) or a comment stating the function expects shape >= 1; keep references to
sampleGamma and calculateBayesianProbabilities so readers can see the invariant.
In `@backend/apps/community/src/experiment/experiment.controller.ts`:
- Around line 416-491: The handler currently always sets startedAt: dayString()
when calling this.experimentService.update (see variable updatedExperiment and
the update call in experiment.controller.ts), which overwrites the original
start time when resuming a PAUSED experiment; change the payload to preserve the
existing experiment.startedAt if present (e.g. set startedAt to
experiment.startedAt ?? dayString()) so startedAt is only set to now for truly
new starts and not when resuming.
- Around line 1063-1073: The ternary in getExposureAttributionSubquery creates a
dead else-branch because MultipleVariantHandling only has FIRST_EXPOSURE and
EXCLUDE and both currently resolve to argMin; simplify variantSelector to always
use 'argMin(variantKey, tuple(created, variantKey))' (remove the unreachable
'any(variantKey)' branch) while leaving multiVariantFilter behavior for
MultipleVariantHandling.EXCLUDE ('HAVING uniqExact(variantKey) = 1') intact so
the function's logic (variantSelector and multiVariantFilter) is clear and not
brittle; update references in getExposureAttributionSubquery and remove the
unnecessary enum branch checks.
- Around line 865-872: The current strict equality check using _sum over
variants' rolloutPercentage (variants.map(...)) can fail due to floating-point
drift; update the validation in the block that computes totalPercentage to
either (a) enforce per-variant integer rollouts by validating each
variant.rolloutPercentage is an integer before summing, or (b) use an
epsilon-based comparison (e.g., Math.abs(totalPercentage - 100) <= EPSILON)
where EPSILON is a small constant (e.g., 1e-6) and include that in the
BadRequestException path; reference the existing identifiers _sum, variants,
rolloutPercentage and the thrown BadRequestException when making the change.
In `@backend/apps/community/src/experiment/experiment.module.ts`:
- Around line 10-22: The module currently relies on forwardRef for
ProjectModule, AnalyticsModule, GoalModule and FeatureFlagModule indicating
cyclic dependencies; break the cycle by extracting the shared logic into a small
module (e.g., create ExperimentVariantSelectionService inside a new
ExperimentSharedModule), move the variant selection/util functions from
ExperimentService into that service, export ExperimentVariantSelectionService
from ExperimentSharedModule and have ExperimentModule, AnalyticsModule,
GoalModule, FeatureFlagModule and ProjectModule import ExperimentSharedModule
instead of referencing each other via forwardRef; update ExperimentModule to
remove forwardRef imports where possible and inject
ExperimentVariantSelectionService into ExperimentService (and any other modules
that need it) to eliminate the dense bidirectional dependency graph.
In `@backend/apps/community/src/experiment/experiment.service.ts`:
- Around line 301-329: Add a projectId filter to findRunningByIds to prevent
cross-tenant exposure: update the method signature (findRunningByIds) to accept
a projectId (string), add a WHERE clause AND projectId = {projectId:String} to
the ClickHouse query and include projectId in query_params, and ensure callers
pass the caller's projectId (or derive it) when invoking findRunningByIds;
similarly consider adding optional projectId filtering to
findOne/findOneWithRelations (and update their callers) so those methods can be
scoped by projectId as an additional defense-in-depth measure.
In `@backend/apps/community/src/feature-flag/feature-flag.controller.ts`:
- Around line 316-348: The code re-sorts experiment.variants with localeCompare
before calling getExperimentVariant which duplicates and can introduce
locale-dependent ordering differences versus the service-side
(getVariantsForExperiments) byte-ordering; remove the re-sort and pass the
variants from experiments directly (or explicitly sort using a byte/ASCII
comparator if you prefer) so the ordering is canonical and deterministic—update
the block around experiment.variants, sortedVariants, and the
getExperimentVariant call in feature-flag.controller.ts (references:
experimentService.findRunningByIds, experiment.variants, getExperimentVariant).
- Around line 408-439: Add ClickHouse async insert settings to both insert calls
to make these fire-and-forget like other ingest paths: when calling
clickhouse.insert for table 'feature_flag_evaluations' (using variable values)
and for table 'experiment_exposures' (using experimentExposures), pass
clickhouse_settings: { async_insert: 1 } in the options object (keep the
existing try/catch logging behavior unchanged).
- Around line 360-385: Currently the code mixes experiment IDs and flag keys in
one map (experimentsByIdOrFlagKey) which is confusing; change the response shape
to have two separate optional records (e.g., response.experiments:
Record<string,string> keyed by experimentId and response.experimentsByFlag:
Record<string,string> keyed by flag key), update the response type declaration
to include both, then in the loop over experimentVariants assign
experiments[experimentId] = variantKey and for each linkedFlag assign
experimentsByFlag[linkedFlag.key] = variantKey (instead of writing both into one
map), and finally set response.experiments and response.experimentsByFlag before
returning.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bfebf7ef-921e-42c9-8cc7-b5ad19a9e560
📒 Files selected for processing (22)
README.mdbackend/apps/community/src/analytics/analytics.controller.tsbackend/apps/community/src/analytics/analytics.module.tsbackend/apps/community/src/app.module.tsbackend/apps/community/src/experiment/bayesian.tsbackend/apps/community/src/experiment/dto/experiment.dto.tsbackend/apps/community/src/experiment/entity/experiment-variant.entity.tsbackend/apps/community/src/experiment/entity/experiment.entity.tsbackend/apps/community/src/experiment/experiment.controller.tsbackend/apps/community/src/experiment/experiment.module.tsbackend/apps/community/src/experiment/experiment.service.tsbackend/apps/community/src/feature-flag/dto/feature-flag.dto.tsbackend/apps/community/src/feature-flag/entity/feature-flag.entity.tsbackend/apps/community/src/feature-flag/evaluation.tsbackend/apps/community/src/feature-flag/feature-flag.controller.tsbackend/apps/community/src/feature-flag/feature-flag.module.tsbackend/apps/community/src/feature-flag/feature-flag.service.tsbackend/apps/community/src/goal/goal.module.tsbackend/migrations/clickhouse/initialise_selfhosted.jsbackend/migrations/clickhouse/selfhosted_2026_05_05_experiments.jsweb/app/api/api.server.tsweb/app/pages/Project/View/ViewProject.tsx
Changes
If applicable, please describe what changes were made in this pull request.
Community Edition support
Database migrations
Documentation
Summary by CodeRabbit
New Features
Documentation
Chores