Skip to content

Comments

feat: Feature flags#450

Merged
Blaumaus merged 22 commits intoswetrix-revampfrom
feature-flags
Dec 10, 2025
Merged

feat: Feature flags#450
Blaumaus merged 22 commits intoswetrix-revampfrom
feature-flags

Conversation

@Blaumaus
Copy link
Member

@Blaumaus Blaumaus commented Dec 8, 2025

Changes

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

Release Notes

  • New Features
    • Added feature flags management system with support for creating, evaluating, and tracking feature flags; includes targeting rules, rollout percentage-based rollouts, and statistical insights on flag performance.
    • Introduced online/offline status indicators for users and sessions, with profile identification tracking throughout analytics.
    • Added sidebar collapse functionality for streamlined project navigation.

✏️ Tip: You can customize this high-level summary in your review settings.

@Blaumaus Blaumaus self-assigned this Dec 8, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 8, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This pull request introduces a comprehensive feature flags system with flag management (creation, retrieval, update, deletion), flag evaluation with targeting rules and rollout percentages, and evaluation tracking. It refactors analytics session recording by renaming recordSession() to recordSessionActivity(), removing the boolean parameter, and simplifying logic. Changes span backend services for both cloud and community deployments, database schema updates (MySQL and ClickHouse), and a complete web UI for managing and viewing feature flags.

Changes

Cohort / File(s) Change Summary
Analytics Refactoring
backend/apps/cloud/src/analytics/analytics.controller.ts, backend/apps/cloud/src/analytics/analytics.service.ts, backend/apps/community/src/analytics/analytics.controller.ts, backend/apps/community/src/analytics/analytics.service.ts
Method renamed from recordSession() to recordSessionActivity() with boolean parameter removed; session-update pathway removed; insertion logic simplified; error messages updated; queries extended to include profileId and isIdentified fields in aggregated outputs.
Feature Flag DTOs
backend/apps/cloud/src/feature-flag/dto/feature-flag.dto.ts, backend/apps/community/src/feature-flag/dto/feature-flag.dto.ts
New DTOs introduced: TargetingRuleDto, CreateFeatureFlagDto, UpdateFeatureFlagDto, EvaluateFeatureFlagsDto, FeatureFlagDto, FeatureFlagStatsDto, EvaluatedFlagsResponseDto, FeatureFlagProfileDto, FeatureFlagProfilesResponseDto with validation and Swagger decorators.
Feature Flag Entities & Types
backend/apps/cloud/src/feature-flag/entity/feature-flag.entity.ts, backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts
New TypeORM entity FeatureFlag (cloud) and interfaces FeatureFlag, ClickhouseFeatureFlag (community); enum FeatureFlagType with BOOLEAN and ROLLOUT; interface TargetingRule with column, filter, and exclusivity fields.
Feature Flag Services
backend/apps/cloud/src/feature-flag/feature-flag.service.ts, backend/apps/community/src/feature-flag/feature-flag.service.ts
New services with CRUD operations, pagination, evaluation logic supporting boolean and rollout types, targeting rule matching with AND semantics, rollout percentage via MD5-based consistent hashing, and helpers for attribute matching.
Feature Flag Controllers & Modules
backend/apps/cloud/src/feature-flag/feature-flag.controller.ts, backend/apps/cloud/src/feature-flag/feature-flag.module.ts, backend/apps/community/src/feature-flag/feature-flag.controller.ts, backend/apps/community/src/feature-flag/feature-flag.module.ts
New controllers with endpoints for listing, retrieving, creating, updating, deleting feature flags; evaluating flags; fetching statistics and profiles; module wiring with TypeORM/ClickHouse integration and dependency injection.
Module Integration
backend/apps/cloud/src/app.module.ts, backend/apps/community/src/app.module.ts, backend/apps/cloud/src/project/entity/project.entity.ts
FeatureFlagModule imported and registered; Project entity extended with featureFlags OneToMany relation.
Database Migrations
backend/migrations/clickhouse/2025_12_02_profile_id.js, backend/migrations/clickhouse/2025_12_08_feature_flags.js, backend/migrations/clickhouse/selfhosted_2025_12_10_feature_flags.js, backend/migrations/mysql/2025_12_08_feature_flags.sql
ClickHouse and MySQL schema changes: added profileId to analytics tables; created feature_flags and feature_flag_evaluations tables; sessions table simplified (removed pageviews/events, added lastSeen).
Web API Integration
web/app/api/index.ts
New feature flag API functions: getProjectFeatureFlags(), getFeatureFlag(), createFeatureFlag(), updateFeatureFlag(), deleteFeatureFlag(), getFeatureFlagStats(), getFeatureFlagProfiles(); new types and constants for feature flag operations.
Web Models & Constants
web/app/lib/models/Project.ts, web/app/lib/constants/index.ts
Session interface extended with profileId and isIdentified fields; featureFlags tab added to project tabs in both self-hosted and production configurations.
Feature Flag UI Pages
web/app/pages/Project/FeatureFlags/View/FeatureFlagsView.tsx, web/app/pages/Project/FeatureFlags/View/FeatureFlagSettingsModal.tsx, web/app/pages/Project/FeatureFlags/View/index.tsx
New components for feature flag listing with pagination, search, and filtering; settings modal for creating/editing flags with targeting rules and implementation help; profile expansion with per-flag statistics and result filtering.
Project View Integration
web/app/pages/Project/View/ViewProject.tsx, web/app/pages/Project/View/components/ProjectSidebar.tsx
Added Feature Flags tab to project navigation; integrated FeatureFlagsView component with period/date range/timezone support; added sidebar collapse feature with localStorage persistence.
Session & User Components
web/app/pages/Project/View/components/Pageflow.tsx, web/app/pages/Project/View/components/SessionDetails.tsx, web/app/pages/Project/View/components/Sessions.tsx, web/app/pages/Project/View/components/Users.tsx, web/app/pages/Project/View/components/SearchFilters.tsx
Added online status indicators and profile display names for sessions; enhanced Pageflow with duration calculations and metadata panel; refactored SearchFilters to use centralized FilterValueInput; added online/offline status visualization.
UI Components
web/app/ui/FilterValueInput.tsx, web/app/ui/Switch.tsx, web/app/ui/Tooltip.tsx
New FilterValueInput component with virtualized dropdown, version handling, country/device/browser icons, and autocomplete; new Switch component for toggles; reduced Tooltip delay from 200 to 50ms.
Localization
web/public/locales/en.json
Added translations for feature flags UI, session metadata (unknownUser), session visualization (endOfSession, properties, online, lastSeenAgo), and sidebar collapse/expand actions.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as FeatureFlagController
    participant Service as FeatureFlagService
    participant DB as Database<br/>(MySQL/TypeORM)
    participant Cache as ClickHouse<br/>(Evaluations)

    rect rgb(200, 150, 255)
    Note over Client,Cache: Flag Evaluation Flow

    Client->>API: POST /evaluate { pid, profileId? }
    activate API
    API->>API: Derive visitor attributes<br/>(IP, geo, device, browser, OS)
    API->>API: Generate profileId if missing
    
    API->>Service: findEnabledByProject(pid)
    activate Service
    Service->>DB: Query enabled flags
    DB-->>Service: [FeatureFlag...]
    deactivate Service
    
    API->>Service: evaluateFlags(flags[], profileId, attributes)
    activate Service
    loop For each flag
        Service->>Service: evaluateFlag(flag, profileId, attributes)
        alt BOOLEAN type
            Service->>Service: matchesTargetingRules()?
            alt Matches
                Service-->>Service: return enabled
            else No match
                Service-->>Service: return false
            end
        else ROLLOUT type
            Service->>Service: matchesTargetingRules()?
            alt Matches
                Service->>Service: isInRolloutPercentage<br/>(MD5 hash)?
                alt In rollout
                    Service-->>Service: return true
                else Not in rollout
                    Service-->>Service: return false
                end
            else No match
                Service-->>Service: return false
            end
        end
    end
    Service-->>API: { flagKey: boolean, ... }
    deactivate Service
    
    API->>API: Prepare evaluation records
    
    par Async Tracking (non-blocking)
        API->>Cache: INSERT evaluations<br/>(pid, flagId, result, profileId)
        Cache-->>API: ✓ OK / × Error logged
    end
    
    API-->>Client: { flagKey: boolean, ... }
    deactivate API
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas requiring extra attention:

  • Feature flag evaluation logic (feature-flag.service.ts in both cloud and community): Verify targeting rule matching semantics (AND vs. OR), rollout percentage calculation using MD5-based hashing, and edge cases with missing attributes.
  • Analytics refactoring across both apps (analytics.service.ts and analytics.controller.ts): Ensure session recording simplification doesn't lose data integrity; verify that profileId and isIdentified fields are correctly propagated through all query paths.
  • React component state management (FeatureFlagsView.tsx, FeatureFlagSettingsModal.tsx): Check lifecycle safety (unmount cleanup, pagination logic, modal state transitions), async operation handling, and error boundaries.
  • Database schema consistency: Verify migrations apply correctly to both MySQL and ClickHouse; ensure feature_flag_evaluations TTL and partitioning align with analytics data retention policies.
  • API permission checks: Confirm that all feature flag endpoints enforce proper authorization via project ownership and manage/view permissions, and that authorization failures are consistent across cloud and community implementations.

Possibly related PRs

  • Profiles / MAU #425: Modifies analytics controller/service session-recording API (recordSessionrecordSessionActivity) with parameter removal and simplified session logic; directly related to the analytics refactoring in this PR.
  • Rework filters experience #423: Updates filter/search UI components (SearchFilters, FilterValueInput) and filter constants; related to the FilterValueInput centralization and SearchFilters refactoring in this PR.
  • feat: Referrer grouping #418: Modifies analytics referrer grouping logic in the service; related through analytics service changes affecting data flow and aggregation in this PR.

Poem

🚩 Flags now flutter high and bright,
Rules and rollouts, rules just right!
Sessions track, profiles align,
Feature gates in grand design.
From cloud to code, a feature divine! 🎉

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Feature flags' clearly and concisely summarizes the main change in the PR: implementing feature flag functionality across both Community and Cloud editions.
Description check ✅ Passed The PR description adequately covers the required template sections: links to relevant issue, confirms Community Edition support, documents that migrations were added, and clarifies that no new public API endpoints were introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Blaumaus Blaumaus marked this pull request as ready for review December 10, 2025 20:26
@Blaumaus
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 10, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/migrations/clickhouse/2025_12_02_profile_id.js (1)

21-35: Replace NOT IN with NOT EXISTS to avoid NULL-handling issues and improve performance.

The NOT IN clause on line 35 has two significant problems:

  1. NULL semantics bug: If any pid or psid in the sessions table is NULL, the NOT IN predicate may return no rows due to SQL's three-valued logic, silently skipping the entire migration.
  2. Performance: NOT IN with subqueries is typically slower than NOT EXISTS or LEFT JOIN ... IS NULL patterns on large datasets.

Apply this diff to use a more reliable pattern:

-  WHERE (sd.pid, sd.psid) NOT IN (SELECT pid, psid FROM ${dbName}.sessions);`,
+  WHERE NOT EXISTS (
+    SELECT 1 FROM ${dbName}.sessions s 
+    WHERE s.pid = sd.pid AND s.psid = sd.psid
+  );`,

Additionally, consider validating the duration field to prevent invalid firstSeen calculations if duration could be NULL or negative.

♻️ Duplicate comments (2)
web/app/pages/Project/View/components/Sessions.tsx (1)

25-45: Duplicate of online status logic in Users.tsx.

This is the same code flagged in Users.tsx. Once extracted to a shared utility, import it here.

backend/apps/community/src/feature-flag/dto/feature-flag.dto.ts (1)

1-245: Same DTO design as cloud module; keep them in sync with shared helpers if possible

This file mirrors the cloud feature‑flag DTOs, which is good for consistency. The same minor notes apply here (e.g., possible @IsInt for rolloutPercentage, and potential to centralize these DTOs in a shared module to avoid duplication).

Functionally this is fine as‑is.

🧹 Nitpick comments (25)
backend/migrations/clickhouse/2025_12_02_profile_id.js (1)

37-38: Good practice: Commented DROP for manual verification.

Leaving the DROP TABLE statement commented is appropriate for a destructive operation. Ensure operational documentation specifies when and how to execute this cleanup step after verifying the migration succeeded.

web/app/ui/Switch.tsx (1)

15-15: Use substring instead of deprecated substr.

String.prototype.substr is deprecated. Use substring for the same result.

-  const switchId = id || `switch-${Math.random().toString(36).substr(2, 9)}`
+  const switchId = id || `switch-${Math.random().toString(36).substring(2, 11)}`
web/app/pages/Project/View/components/Users.tsx (1)

22-42: Extract shared online status logic to a utility.

The getOnlineStatus function, OnlineStatus type, and threshold constants are duplicated in Sessions.tsx (lines 25-45). Consider extracting to a shared utility file (e.g., ~/utils/onlineStatus.ts) to ensure consistency and reduce maintenance burden.

// ~/utils/onlineStatus.ts
export const ONLINE_THRESHOLD_MINUTES = 5
export const RECENTLY_ACTIVE_THRESHOLD_MINUTES = 30

export type OnlineStatus = 'online' | 'recently_active' | 'offline'

export const getOnlineStatus = (lastSeen: string): OnlineStatus => {
  const now = dayjs()
  const lastSeenTime = dayjs(lastSeen)
  const minutesAgo = now.diff(lastSeenTime, 'minute')

  if (minutesAgo < ONLINE_THRESHOLD_MINUTES) return 'online'
  if (minutesAgo < RECENTLY_ACTIVE_THRESHOLD_MINUTES) return 'recently_active'
  return 'offline'
}
web/app/pages/Project/FeatureFlags/View/FeatureFlagSettingsModal.tsx (3)

94-126: Consider stabilizing fetchFilterValues callback dependencies.

The useCallback includes filterValuesCache and loadingColumns in its dependency array. Since these change on every fetch, the callback reference changes frequently. While the guard conditions prevent redundant fetches, this could cause unnecessary effect re-runs.

Consider using refs to access the latest cache/loading state without adding them to dependencies:

+ const filterValuesCacheRef = useRef(filterValuesCache)
+ filterValuesCacheRef.current = filterValuesCache
+ const loadingColumnsRef = useRef(loadingColumns)
+ loadingColumnsRef.current = loadingColumns

  const fetchFilterValues = useCallback(
    async (column: string) => {
-     if (filterValuesCache[column] || loadingColumns.has(column)) return
+     if (filterValuesCacheRef.current[column] || loadingColumnsRef.current.has(column)) return
      // ...
    },
-   [projectId, filterValuesCache, loadingColumns],
+   [projectId],
  )

199-213: Consider type-safe approach for dynamic field assignment.

The @ts-expect-error comment can be avoided with a more type-safe approach:

  const updateTargetingRule = (index: number, field: keyof TargetingRule, value: string | boolean) => {
    const updated = [...targetingRules]
    if (field === 'column') {
      updated[index] = { ...updated[index], column: value as string, filter: '' }
      if (value && !filterValuesCache[value as string] && !loadingColumns.has(value as string)) {
        fetchFilterValues(value as string)
      }
+   } else if (field === 'filter') {
+     updated[index] = { ...updated[index], filter: value as string }
+   } else if (field === 'isExclusive') {
+     updated[index] = { ...updated[index], isExclusive: value as boolean }
    } else {
-     // @ts-expect-error - TypeScript doesn't like dynamic field assignment
-     updated[index][field] = value
+     updated[index] = { ...updated[index], [field]: value }
    }
    setTargetingRules(updated)
  }

296-302: Add aria-label to close button for accessibility.

The close button has an icon but no accessible label:

  <button
    type='button'
    onClick={onClose}
+   aria-label={t('common.close')}
    className='rounded-md p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:hover:bg-slate-800 dark:hover:text-gray-300'
  >
    <XMarkIcon className='h-5 w-5' />
  </button>
web/app/pages/Project/FeatureFlags/View/FeatureFlagsView.tsx (2)

661-667: Consider batching stats requests for performance.

When flags changes, loadFlagStats is called for each flag individually (line 663-665), which could result in many parallel API requests if there are many flags. Consider implementing a batch endpoint or adding request throttling:

// Option 1: Batch endpoint (preferred, requires backend change)
// Option 2: Sequential loading with concurrency limit
const loadAllFlagStats = async () => {
  const BATCH_SIZE = 5
  for (let i = 0; i < flags.length; i += BATCH_SIZE) {
    const batch = flags.slice(i, i + BATCH_SIZE)
    await Promise.all(batch.map(flag => loadFlagStats(flag.id)))
  }
}

706-716: Consider static import for updateFeatureFlag.

The dynamic import of updateFeatureFlag on line 708 adds unnecessary complexity. Since this function is likely to be used (toggle is a common action), a static import would be simpler and more consistent with how deleteFeatureFlagApi is imported:

  import {
    deleteFeatureFlag as deleteFeatureFlagApi,
    getProjectFeatureFlags,
    getFeatureFlagStats,
    getFeatureFlagProfiles,
+   updateFeatureFlag,
    // ...
  } from '~/api'

  const handleToggleFlag = async (flagId: string, enabled: boolean) => {
    try {
-     const { updateFeatureFlag } = await import('~/api')
      await updateFeatureFlag(flagId, { enabled })
web/public/locales/en.json (1)

1175-1176: Online status strings look good but watch pluralisation

project.online and project.lastSeenAgo are clear and match intended usage for presence indicators. If you later expose “X users online” or similar, you may want separate pluralisable keys, but for now these are fine.

web/app/pages/Project/View/ViewProject.tsx (2)

1691-1694: Refresh flow for feature flags correctly uses a dedicated trigger

In refreshStats, the new branch for PROJECT_TABS.featureFlags increments featureFlagsRefreshTrigger instead of reloading analytics. This mirrors the existing pattern for captcha/goals and lets FeatureFlagsView decide how to react to refreshes.

Just ensure FeatureFlagsView actually subscribes to featureFlagsRefreshTrigger via useViewProjectContext, otherwise the trigger will be a no‑op.


4031-4033: SessionDetails now receives pages; consider minor type‑safety tightening

Passing pages={activeSession?.pages} into SessionDetails enables its new duration fallback logic based on page timestamps and is functionally correct, given it already guards on activeSession?.details.

If SessionDetails expects non‑nullable details, you could tighten this slightly:

- {activeSession?.details ? (
-   <SessionDetails details={activeSession?.details} pages={activeSession?.pages} />
- ) : null}
+ {activeSession ? (
+   <SessionDetails details={activeSession.details} pages={activeSession.pages} />
+ ) : null}

to keep TypeScript happy and avoid passing potentially‑undefined details.

backend/migrations/clickhouse/selfhosted_2025_12_10_feature_flags.js (1)

6-20: Consider using UInt8 for the enabled column.

The enabled column is defined as Int8 DEFAULT 1, which allows negative values. Since this represents a boolean state, UInt8 would be more semantically appropriate and consistent with the result column in feature_flag_evaluations.

This is a minor consistency suggestion and not blocking.

-    enabled Int8 DEFAULT 1,
+    enabled UInt8 DEFAULT 1,
backend/apps/cloud/src/analytics/analytics.service.ts (2)

1221-1266: Session recording logic is sound but may accumulate multiple rows per session over time

The recordSessionActivity implementation correctly preserves the original firstSeen and updates lastSeen, but every call inserts a new row instead of updating/merging an existing one. Depending on the sessions table engine, this can:

  • Produce multiple physical rows per (pid, psid) and make LIMIT 1 consumers like getSessionDurationFromClickHouse sensitive to merge order.
  • Inflate storage and make aggregates slightly harder to reason about if the engine doesn’t collapse rows by primary key.

If the intention is “one logical row per session,” consider either:

  • Using a Replacing/Versioned engine keyed by (pid, psid) with FINAL everywhere you read, or
  • Changing this to an upsert pattern (e.g., aggregating in queries with argMin(firstSeen), argMax(lastSeen) or similar) so downstream callers don’t depend on implicit engine behavior.

Given it’s analytics-only, this is not a blocker but worth confirming against the actual table schema.


4145-4165: New profileId / isIdentified fields in sessions list look good; handle missing session rows explicitly

Joining session_duration_agg into getSessionsList to expose sdur, profileId, and isIdentified is a nice addition. Two minor points to consider:

  • For sessions that predate the sessions table or where recordSessionActivity failed, sda.avg_duration and sda.profileId will be NULL. If the UI expects numbers/booleans, you may want to COALESCE avg_duration to 0 and explicitly treat NULL profileId as anonymous instead of relying on ClickHouse’s startsWith behavior on nullables.
  • You already reference AnalyticsService.PROFILE_PREFIX_USER in the query; this keeps the “identified” definition consistent with the TS helper, which is good. Just ensure the profileId column in sessions is Nullable(String) or String and always set for new data so isIdentified is reliable going forward.

Behavior is otherwise coherent with the rest of the analytics API.

backend/apps/community/src/analytics/analytics.controller.ts (1)

926-930: Consistent use of recordSessionActivity; consider moving it after uniqueness rejection in log

Using recordSessionActivity(psid, pid, profileId) across custom events, heartbeat, pageviews, noscript, and errors is consistent with the new sessions model and keeps profileId in sync.

One small refinement: in log the session activity is recorded before the unique/ForbiddenException check. That means requests you ultimately reject as “not unique” will still refresh lastSeen in sessions. If you want session tracking to reflect only accepted events, you could move the recordSessionActivity call to just after the uniqueness check.

Functionality is otherwise correct.

Also applies to: 1013-1015, 1048-1052, 1194-1198, 1501-1505

backend/apps/cloud/src/feature-flag/dto/feature-flag.dto.ts (1)

1-245: DTO shapes look solid; minor validation/DRY opportunities

The DTOs and validation rules look consistent with the feature‑flag entity and are generally good to go. A couple of small improvements to consider:

  • If rolloutPercentage is intended to be an integer 0–100, you could tighten validation with @IsInt() and (optionally) @Type(() => Number) rather than a generic @IsNumber().
  • Cloud and Community DTOs are effectively identical; if you expect them to stay in lockstep, a shared package/module for these types would reduce duplication and the risk of drift.

Nothing here blocks merging.

backend/apps/community/src/feature-flag/feature-flag.controller.ts (3)

194-278: Public /evaluate endpoint is coherent; confirm suspension/blacklist behavior and evaluation logging assumptions

The evaluation flow is clean: you load enabled flags, derive visitor attributes, generate a profileId via AnalyticsService.generateProfileId, evaluate flags, and then track evaluations asynchronously into ClickHouse.

Two behavioral points to double‑check:

  • Unlike analytics ingestion, this endpoint doesn’t apply checkIfAccountSuspended, origin/IP/country blacklist, or bot detection – it only checks project.active. If billing suspension or blocking rules should also prevent feature‑flag evaluation, it’s worth reusing the same checks here; otherwise, clarifying that flags remain active when analytics is blocked would help future maintainers.
  • trackEvaluations writes one row per flags entry and treats evaluatedFlags[flag.key] as falsey → 0. If featureFlagService.evaluateFlags ever omits a key (e.g., in case of internal filtering), those flags will be logged as result = 0. If that’s not desired, consider skipping flags not present in the result map.

Functionally fine, but aligning these behaviors explicitly will avoid subtle surprises later.

Also applies to: 280-309


529-537: Profiles listing endpoint is solid; reuse profile prefix constant for identified detection

The /feature-flag/:id/profiles endpoint correctly:

  • Reuses analytics time‑bucket helpers for the date window.
  • Caps take to <= 50 and defaults sensibly.
  • Filters by last evaluation result via a HAVING clause, which is safe given the controlled result values.

One small consistency tweak: when mapping profiles, you currently do:

isIdentified: row.profileId.startsWith('usr_'),

Elsewhere (AnalyticsService and sessions queries) you centralize this via PROFILE_PREFIX_USER. Reusing that constant here:

isIdentified: row.profileId.startsWith(AnalyticsService.PROFILE_PREFIX_USER),

would keep the “identified profile” definition consistent across analytics and feature flags.

Also applies to: 549-601, 619-631


139-191: Create/update flows handle quotas and duplicate keys correctly

The create and update handlers correctly:

  • Enforce a per‑project flag limit via FEATURE_FLAGS_MAXIMUM.
  • Check for duplicate key on both create and key changes during update.
  • Enforce project‑level allowedToManage before mutating flags.

The error mapping to BadRequestException for unknown persistence errors is a policy choice; if you ever need clearer 5xx vs 4xx semantics, you could switch those catch‑blocks to InternalServerErrorException while retaining the same generic message.

No blocking issues here.

Also applies to: 323-372

backend/apps/cloud/src/feature-flag/feature-flag.service.ts (1)

73-83: Return type for update and delete methods is any.

Using Promise<any> obscures the actual return type. TypeORM's update returns UpdateResult and delete returns DeleteResult. Typing these explicitly improves maintainability.

- async update(id: string, flagData: Partial<FeatureFlag>): Promise<any> {
+ async update(id: string, flagData: Partial<FeatureFlag>): Promise<UpdateResult> {
    return this.featureFlagRepository.update(id, flagData)
  }

- async delete(id: string): Promise<any> {
+ async delete(id: string): Promise<DeleteResult> {
    return this.featureFlagRepository.delete(id)
  }

Add import:

- import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'
+ import { FindManyOptions, FindOneOptions, Repository, UpdateResult, DeleteResult } from 'typeorm'
backend/apps/cloud/src/feature-flag/feature-flag.controller.ts (4)

110-117: Consider extracting the flag-to-DTO transformation.

The pattern of mapping flags with _omit(flag, ['project']) and adding pid is repeated across multiple endpoints (getProjectFeatureFlags, getFeatureFlag, createFeatureFlag, updateFeatureFlag). Consider extracting to a private helper for consistency.

private toFlagDto(flag: FeatureFlag & { project?: { id: string } }): FeatureFlagDto {
  return {
    ..._omit(flag, ['project']),
    pid: flag.project?.id,
  }
}

204-209: HTTP status code may be semantically incorrect for quota limit.

HttpStatus.PAYMENT_REQUIRED (402) is used when hitting the 50 flags per project limit. However, this is a resource quota limit, not a billing issue. Consider using HttpStatus.FORBIDDEN (403) or HttpStatus.UNPROCESSABLE_ENTITY (422) with a clear message instead.

    if (flagsCount >= FEATURE_FLAGS_MAXIMUM) {
      throw new HttpException(
        `You cannot create more than ${FEATURE_FLAGS_MAXIMUM} feature flags per project.`,
-       HttpStatus.PAYMENT_REQUIRED,
+       HttpStatus.FORBIDDEN,
      )
    }

322-332: Floating promise with .catch() may silently fail.

The trackEvaluations() call uses .catch() without awaiting, which is intentional for fire-and-forget. However, the error is only logged. Consider adding monitoring/metrics for tracking failures to detect systematic issues.


541-551: Silently returning empty stats on error may mask issues.

While graceful degradation is good, returning zero stats on any error (including schema issues, connection problems) makes debugging harder. Consider differentiating between "no data" vs "error fetching data" scenarios.

backend/apps/community/src/feature-flag/feature-flag.service.ts (1)

63-79: Unsafe type assertion with as any.

Line 66 uses as any to bypass type checking, which can hide type errors. Consider building the result object more explicitly.

  formatFlagToClickhouse(
    flag: Partial<FeatureFlag>,
  ): Partial<ClickhouseFeatureFlag> {
-   const result: Partial<ClickhouseFeatureFlag> = { ...flag } as any
+   const result: Partial<ClickhouseFeatureFlag> = {}
+   
+   if (flag.id !== undefined) result.id = flag.id
+   if (flag.key !== undefined) result.key = flag.key
+   if (flag.description !== undefined) result.description = flag.description
+   if (flag.flagType !== undefined) result.flagType = flag.flagType
+   if (flag.rolloutPercentage !== undefined) result.rolloutPercentage = flag.rolloutPercentage
+   if (flag.projectId !== undefined) result.projectId = flag.projectId
+   if (flag.created !== undefined) result.created = flag.created

    if (flag.targetingRules !== undefined) {
      result.targetingRules = flag.targetingRules
        ? JSON.stringify(flag.targetingRules)
        : null
    }

    if (flag.enabled !== undefined) {
      result.enabled = flag.enabled ? 1 : 0
    }

    return result
  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 982ec5d and 365392d.

📒 Files selected for processing (38)
  • backend/apps/cloud/src/analytics/analytics.controller.ts (5 hunks)
  • backend/apps/cloud/src/analytics/analytics.service.ts (4 hunks)
  • backend/apps/cloud/src/app.module.ts (2 hunks)
  • backend/apps/cloud/src/feature-flag/dto/feature-flag.dto.ts (1 hunks)
  • backend/apps/cloud/src/feature-flag/entity/feature-flag.entity.ts (1 hunks)
  • backend/apps/cloud/src/feature-flag/feature-flag.controller.ts (1 hunks)
  • backend/apps/cloud/src/feature-flag/feature-flag.module.ts (1 hunks)
  • backend/apps/cloud/src/feature-flag/feature-flag.service.ts (1 hunks)
  • backend/apps/cloud/src/project/entity/project.entity.ts (2 hunks)
  • backend/apps/community/src/analytics/analytics.controller.ts (5 hunks)
  • backend/apps/community/src/analytics/analytics.service.ts (4 hunks)
  • backend/apps/community/src/app.module.ts (2 hunks)
  • backend/apps/community/src/feature-flag/dto/feature-flag.dto.ts (1 hunks)
  • backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1 hunks)
  • backend/apps/community/src/feature-flag/feature-flag.controller.ts (1 hunks)
  • backend/apps/community/src/feature-flag/feature-flag.module.ts (1 hunks)
  • backend/apps/community/src/feature-flag/feature-flag.service.ts (1 hunks)
  • backend/migrations/clickhouse/2025_12_02_profile_id.js (1 hunks)
  • backend/migrations/clickhouse/2025_12_08_feature_flags.js (1 hunks)
  • backend/migrations/clickhouse/selfhosted_2025_12_10_feature_flags.js (1 hunks)
  • backend/migrations/mysql/2025_12_08_feature_flags.sql (1 hunks)
  • web/app/api/index.ts (1 hunks)
  • web/app/lib/constants/index.ts (2 hunks)
  • web/app/lib/models/Project.ts (1 hunks)
  • web/app/pages/Project/FeatureFlags/View/FeatureFlagSettingsModal.tsx (1 hunks)
  • web/app/pages/Project/FeatureFlags/View/FeatureFlagsView.tsx (1 hunks)
  • web/app/pages/Project/FeatureFlags/View/index.tsx (1 hunks)
  • web/app/pages/Project/View/ViewProject.tsx (11 hunks)
  • web/app/pages/Project/View/components/Pageflow.tsx (4 hunks)
  • web/app/pages/Project/View/components/ProjectSidebar.tsx (10 hunks)
  • web/app/pages/Project/View/components/SearchFilters.tsx (2 hunks)
  • web/app/pages/Project/View/components/SessionDetails.tsx (3 hunks)
  • web/app/pages/Project/View/components/Sessions.tsx (5 hunks)
  • web/app/pages/Project/View/components/Users.tsx (2 hunks)
  • web/app/ui/FilterValueInput.tsx (1 hunks)
  • web/app/ui/Switch.tsx (1 hunks)
  • web/app/ui/Tooltip.tsx (1 hunks)
  • web/public/locales/en.json (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (19)
backend/apps/cloud/src/feature-flag/feature-flag.module.ts (2)
backend/apps/cloud/src/app.module.ts (1)
  • Module (93-107)
backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1)
  • FeatureFlag (12-22)
backend/migrations/clickhouse/2025_12_08_feature_flags.js (1)
backend/migrations/clickhouse/setup.js (2)
  • dbName (107-107)
  • queriesRunner (76-105)
web/app/pages/Project/View/components/Sessions.tsx (2)
web/app/utils/profileAvatars.tsx (2)
  • getProfileDisplayName (190-199)
  • ProfileAvatar (210-216)
web/app/ui/Badge.tsx (1)
  • Badge (11-34)
web/app/ui/Switch.tsx (1)
web/app/ui/Text.tsx (1)
  • Text (51-75)
web/app/pages/Project/FeatureFlags/View/FeatureFlagsView.tsx (7)
web/app/api/index.ts (9)
  • FeatureFlagProfile (1351-1357)
  • ProjectFeatureFlag (1256-1266)
  • FeatureFlagStats (1268-1274)
  • DEFAULT_FEATURE_FLAGS_TAKE (1286-1286)
  • getProjectFeatureFlags (1288-1301)
  • getFeatureFlagStats (1335-1349)
  • getFeatureFlagProfiles (1363-1388)
  • DEFAULT_FEATURE_FLAG_PROFILES_TAKE (1359-1359)
  • updateFeatureFlag (1319-1325)
web/app/utils/profileAvatars.tsx (2)
  • getProfileDisplayName (190-199)
  • ProfileAvatar (210-216)
web/app/ui/Badge.tsx (1)
  • Badge (11-34)
web/app/ui/Text.tsx (1)
  • Text (51-75)
web/app/ui/Switch.tsx (1)
  • Switch (14-68)
web/app/providers/CurrentProjectProvider.tsx (1)
  • useCurrentProject (244-252)
web/app/pages/Project/View/ViewProject.tsx (1)
  • useViewProjectContext (447-450)
web/app/ui/FilterValueInput.tsx (1)
web/app/lib/constants/index.ts (3)
  • BROWSER_LOGO_MAP (865-912)
  • OS_LOGO_MAP (914-940)
  • OS_LOGO_MAP_DARK (942-947)
web/app/pages/Project/View/components/Pageflow.tsx (2)
web/fly_import_env.cjs (1)
  • value (14-14)
web/app/utils/generic.ts (2)
  • getStringFromTime (81-91)
  • getTimeFromSeconds (64-79)
web/app/pages/Project/FeatureFlags/View/FeatureFlagSettingsModal.tsx (5)
web/app/providers/ThemeProvider.tsx (1)
  • useTheme (29-37)
web/app/api/index.ts (2)
  • TargetingRule (1250-1254)
  • getFilters (1725-1735)
backend/apps/cloud/src/feature-flag/entity/feature-flag.entity.ts (1)
  • TargetingRule (18-22)
web/app/ui/FilterValueInput.tsx (1)
  • filterCategoryIcons (48-68)
web/app/ui/Text.tsx (1)
  • Text (51-75)
backend/apps/community/src/feature-flag/feature-flag.module.ts (1)
backend/apps/community/src/app.module.ts (1)
  • Module (74-78)
backend/apps/cloud/src/feature-flag/dto/feature-flag.dto.ts (2)
web/app/api/index.ts (1)
  • TargetingRule (1250-1254)
backend/apps/cloud/src/feature-flag/entity/feature-flag.entity.ts (1)
  • TargetingRule (18-22)
web/app/pages/Project/View/components/Users.tsx (1)
web/app/utils/profileAvatars.tsx (1)
  • ProfileAvatar (210-216)
backend/apps/community/src/feature-flag/dto/feature-flag.dto.ts (1)
backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1)
  • TargetingRule (6-10)
backend/apps/community/src/feature-flag/feature-flag.controller.ts (2)
backend/apps/community/src/feature-flag/dto/feature-flag.dto.ts (7)
  • FeatureFlagDto (166-193)
  • CreateFeatureFlagDto (36-98)
  • EvaluatedFlagsResponseDto (212-218)
  • EvaluateFeatureFlagsDto (152-164)
  • UpdateFeatureFlagDto (100-150)
  • FeatureFlagStatsDto (195-210)
  • FeatureFlagProfilesResponseDto (239-245)
backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1)
  • FeatureFlag (12-22)
web/app/pages/Project/View/components/ProjectSidebar.tsx (2)
web/app/lib/constants/index.ts (1)
  • PROJECT_TABS (425-427)
web/app/ui/Text.tsx (1)
  • Text (51-75)
backend/apps/cloud/src/feature-flag/feature-flag.controller.ts (2)
backend/apps/cloud/src/feature-flag/dto/feature-flag.dto.ts (7)
  • FeatureFlagDto (166-193)
  • CreateFeatureFlagDto (36-98)
  • EvaluatedFlagsResponseDto (212-218)
  • EvaluateFeatureFlagsDto (152-164)
  • UpdateFeatureFlagDto (100-150)
  • FeatureFlagStatsDto (195-210)
  • FeatureFlagProfilesResponseDto (239-245)
backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1)
  • FeatureFlag (12-22)
web/app/pages/Project/View/ViewProject.tsx (4)
web/app/lib/constants/index.ts (1)
  • PROJECT_TABS (425-427)
web/app/pages/Project/View/components/SessionDetails.tsx (1)
  • SessionDetails (27-230)
web/app/lib/models/Project.ts (1)
  • SessionDetails (106-125)
web/app/pages/Project/View/ViewProject.helpers.tsx (1)
  • getFormatDate (1944-1944)
backend/apps/community/src/feature-flag/feature-flag.service.ts (2)
backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (3)
  • ClickhouseFeatureFlag (24-34)
  • FeatureFlag (12-22)
  • TargetingRule (6-10)
backend/apps/community/src/common/pagination/pagination.results.interface.ts (1)
  • PaginationOptionsInterface (8-11)
backend/apps/cloud/src/project/entity/project.entity.ts (1)
backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1)
  • FeatureFlag (12-22)
backend/migrations/clickhouse/2025_12_02_profile_id.js (1)
backend/migrations/clickhouse/setup.js (1)
  • dbName (107-107)
🔇 Additional comments (80)
backend/migrations/clickhouse/2025_12_02_profile_id.js (2)

4-6: LGTM: Idempotent column additions.

The profileId column additions use IF NOT EXISTS and Nullable(String) appropriately, allowing for safe re-execution and gradual feature rollout.


9-19: No action required. This migration safely creates a new sessions table.

The IF NOT EXISTS clause is appropriate here because this is the initial creation of the sessions table, not a modification of an existing one. The INSERT statement includes a WHERE (sd.pid, psid) NOT IN (SELECT pid, psid FROM sessions) clause to prevent duplicate data on re-runs. The ReplacingMergeTree engine with lastSeen as the version column properly handles row versioning for subsequent updates. The commented DROP statement for session_durations shows appropriate caution for manual verification before cleanup.

Likely an incorrect or invalid review comment.

web/app/ui/Tooltip.tsx (1)

39-39: LGTM - Faster tooltip delay improves UX for collapsed sidebar.

The reduced delay from 200ms to 50ms makes tooltips more responsive, which is particularly useful for the new collapsed sidebar state where icon-only tabs rely on tooltips for labels.

web/app/ui/Switch.tsx (1)

19-65: Well-implemented accessible switch component.

Good use of ARIA attributes (role="switch", aria-checked), proper label association via htmlFor, and appropriate disabled state handling for both the button and label.

web/app/pages/Project/View/components/ProjectSidebar.tsx (4)

219-232: LGTM - SSR-safe localStorage initialization with memoized toggle.

The typeof window !== 'undefined' check properly handles SSR, and useCallback prevents unnecessary re-renders of child components that receive toggleCollapsed.


81-134: Collapsed view implementation is correct.

The duplicated rendering logic between collapsed and expanded modes is acceptable given the significantly different layouts (icon-only with tooltips vs. full labels with collapsible groups).


399-441: Bottom section properly implements collapse toggle with accessibility.

Good use of aria-label with internationalized strings for screen reader support, and the conditional text rendering based on isCollapsed state maintains usability in both modes.


30-30: LGTM - Feature flags icon color follows existing pattern.

web/app/api/index.ts (3)

1249-1266: LGTM - Well-typed interfaces for feature flags.

The interfaces follow the existing patterns in the codebase and properly type the feature flag entities with appropriate nullable fields (description, targetingRules).


1288-1333: LGTM - CRUD operations follow existing API patterns.

The feature flag API functions consistently use the same error handling pattern and return type annotations as existing endpoints in this file.


1363-1388: LGTM - Profile fetching with proper filter handling.

The result filter correctly converts 'all' to undefined to avoid sending unnecessary query parameters, keeping the URL clean.

web/app/pages/Project/FeatureFlags/View/index.tsx (1)

1-1: LGTM - Standard barrel export.

Clean re-export pattern that allows importing the view component via the directory path.

web/app/lib/models/Project.ts (1)

101-104: LGTM! Type additions align with backend data shape.

The profileId and isIdentified fields extend the session model appropriately. Note that Session.isIdentified uses 1 | 0 (consistent with isLive above) while Profile.isIdentified at line 129 uses boolean. This is likely intentional if Session data comes raw from ClickHouse (UInt8) while Profile data is transformed, but worth verifying this is the intended pattern.

web/app/lib/constants/index.ts (2)

408-408: LGTM! Feature flags tab added to selfhosted configuration.


419-419: LGTM! Feature flags tab added to production configuration.

web/app/pages/Project/View/components/Users.tsx (2)

84-86: LGTM! Memoization is appropriate here.

Memoizing onlineStatus and lastSeenAgo based on profile.lastSeen prevents unnecessary recalculations during re-renders.


95-113: LGTM! Clean implementation of the online status indicator.

The conditional rendering for offline status (no indicator) and the tooltip for online/recently_active statuses provides good UX feedback.

web/app/pages/Project/View/components/Sessions.tsx (4)

85-90: LGTM! Handles missing profileId gracefully.

The fallback to t('project.unknownUser') when profileId is null provides clear UX for sessions without an associated profile.


92-121: LGTM! Comprehensive date/time formatting logic.

The dateLineString computation handles live sessions (start time only) and completed sessions (start-end with duration) appropriately. The memoization dependencies correctly include all variables that affect the output.


134-158: LGTM! Avatar with fallback for unknown users.

The placeholder avatar with "?" for sessions without a profileId provides clear visual differentiation from identified users.


162-163: LGTM! Consistent identified badge implementation.

Matches the pattern used in Users.tsx for displaying the identified badge.

web/app/pages/Project/View/components/SearchFilters.tsx (3)

4-5: LGTM! Import cleanup aligns with centralized FilterValueInput.

Removing the broad icon imports and unused hooks in favor of the centralized FilterValueInput component reduces bundle size and code duplication.


13-13: LGTM! Clean import of centralized filter input utilities.

Importing filterCategoryIcons, createVersionValue, and parseVersionValue from FilterValueInput consolidates the filter-related logic appropriately.


456-466: LGTM! Clean integration of FilterValueInput component.

The component receives all necessary props for proper functionality: cached filter values, language for localization, loading state, and theme for styling. This is a good refactor that centralizes the filter value input logic.

web/app/pages/Project/View/components/SessionDetails.tsx (3)

16-25: LGTM! Clean interface definition and prop extension.

The PageflowItem interface and updated SessionDetailsProps are well-defined. Consider exporting PageflowItem if it needs to be shared with other components (e.g., Pageflow.tsx which has a similar structure).


33-53: Well-implemented fallback duration calculation.

The useMemo correctly prioritizes details.sdur and falls back to calculating duration from pageview timestamps. The dependency array is appropriate.


209-227: LGTM! Rendering correctly uses computed session duration.

The conditional rendering properly displays the live indicator, formatted duration, or 'N/A' fallback based on the computed sessionDuration.

web/app/pages/Project/FeatureFlags/View/FeatureFlagSettingsModal.tsx (5)

37-49: LGTM! Well-organized constants.

The FLAG_TYPES and TARGETING_COLUMNS arrays are clearly defined and provide good labels for the UI.


147-156: LGTM! Modal initialization logic is correct.

The effect properly handles both create (reset form) and edit (load flag data) modes. The eslint-disable comment is acceptable here since loadFlag should only run when flagId changes, not on every render.


158-193: LGTM! Form submission logic is well-structured.

The handleSubmit correctly differentiates between create and update operations, properly handles the rollout percentage based on flag type, and includes appropriate error handling with user feedback via toasts.


219-226: LGTM! Good key normalization.

The key transformation enforces a consistent kebab-case format with allowed characters, which is appropriate for feature flag identifiers that may be used in code.


364-553: LGTM! Well-implemented targeting rules UI.

The targeting rules section provides a clean interface with column selection (using icons from filterCategoryIcons), include/exclude operator toggle, and autocomplete value input with loading states. The delete functionality is clear and accessible.

web/app/pages/Project/FeatureFlags/View/FeatureFlagsView.tsx (6)

63-130: LGTM! Clean profile row component.

The FeatureFlagProfileRow component is well-structured with memoized computed values and proper accessibility (link navigation, visual indicators).


150-418: LGTM! Comprehensive flag row component.

The FeatureFlagRow component handles:

  • Flag display with status/type badges
  • Responsive stats (mobile vs desktop)
  • Action buttons with proper event propagation handling
  • Expandable profiles section with filtering
  • Delete confirmation modal

Well-structured with good UX patterns.


432-460: LGTM! Proper state management with unmount safety.

The use of isMountedRef and isLoadingRef correctly prevents state updates after unmount and concurrent data loading, which is a good pattern for async operations in React.


473-496: LGTM! Robust data loading with guards.

The loadFlags function properly prevents concurrent loads and checks mount status before state updates. The error handling gracefully extracts the error message with fallbacks.


555-625: Well-designed auto-filter selection on expand.

The handleToggleExpand function intelligently selects the result filter that has data (preferring 'true', falling back to 'false'), providing a better initial UX. The error handling is comprehensive.


718-762: LGTM! Good error and loading state handling.

The error state provides clear feedback with recovery options (reload page, contact support). The loading logic correctly distinguishes between initial load (full loader) and subsequent loads (loading bar).

web/app/pages/Project/View/components/Pageflow.tsx (5)

41-99: LGTM! Clean metadata panel with progressive disclosure.

The MetadataPanel component provides a good UX with:

  • Limited initial display (5 items) to avoid overwhelming users
  • Show more/less toggle for full content
  • Truncation with tooltips for long values
  • Proper keyboard accessibility for the toggle button

101-107: LGTM! Clean duration formatting helper.

The formatDuration function correctly handles edge cases and delegates to existing utility functions for consistent formatting.


109-234: LGTM! Well-implemented pageflow item with accessibility.

The PageflowItem component provides:

  • Accessible expand/collapse for metadata (keyboard support)
  • Visual differentiation by event type (pageview, event, error)
  • Timeline visualization (solid line to next event, dashed to session end)
  • Time duration display between events

257-285: LGTM! Correct time calculation logic.

The timeBetweenEvents and timeAfterLastEvent memoized calculations are well-implemented:

  • timeBetweenEvents correctly computes inter-event durations
  • timeAfterLastEvent properly calculates remaining time based on session duration minus the span of recorded events
  • Both handle edge cases (empty pages, single event, sdur <= 0)

326-366: LGTM! Clear end-of-session visual indicator.

The end-of-session marker provides a clear visual termination point for the timeline, with optional display of remaining session time. The hollow circle icon distinguishes it from event icons.

web/app/ui/FilterValueInput.tsx (5)

47-92: LGTM! Well-organized exports for filter UI.

The exported icon mappings (filterCategoryIcons, deviceIconMapping) and version value helpers provide a clean API for consumers. The VERSION_SEPARATOR pattern handles the edge case of browser/OS versions that need to display parent name alongside version.


154-225: LGTM! Comprehensive icon rendering logic.

The getIconForItem callback handles multiple column types with appropriate icons:

  • Country flags for cc
  • Browser logos for br/brv with version parsing
  • OS logos for os/osv with dark mode support
  • Device icons for dv
  • Fallback globe/question icons for unknowns

266-292: LGTM! Good keyboard handling with country code conversion.

The Enter key handling is well-implemented:

  • For country codes, it converts country names to alpha-2 codes using countries.getAlpha2Code
  • Falls back to matching existing items or using the raw input
  • Escape properly closes the dropdown and clears input

356-399: LGTM! Efficient virtualized list implementation.

The virtualized list using @tanstack/react-virtual ensures good performance even with large item lists. The overscan of 15 items provides smooth scrolling, and the absolute positioning with transforms is the correct pattern for virtualization.


333-355: LGTM! Good empty state with "use value" action.

The empty state provides a clear path forward when no matches are found:

  • Allows users to use their custom input value
  • Maintains country code conversion for consistency
  • Falls back to a simple "nothing found" message when input is empty
web/public/locales/en.json (3)

120-121: New generic “collapse/expand” labels look fine

The added common.collapse / common.expand keys are clear, generic, and consistent with existing common actions.


889-890: Dashboard feature flags entry strings are consistent with new tab

dashboard.featureFlags / dashboard.featureFlagsDesc align with the new project tab and describe the feature succinctly; no issues here.


1095-1095: project.unknownUser is a useful complement to unknownCountry

The new project.unknownUser key matches existing patterns (e.g. unknownCountry) and should integrate cleanly with user/profile views.

web/app/pages/Project/View/ViewProject.tsx (9)

19-45: Lucide FlagIcon import is consistent with existing icon usage

Adding FlagIcon alongside other lucide icons keeps icon usage uniform across tabs; no issues here.


170-175: FeatureFlagsView import mirrors existing Goals/Alerts structure

Importing FeatureFlagsView from ../FeatureFlags/View matches the existing pattern used for Goals and Alerts, keeping the project‑level feature modules organised.


396-413: Context extended cleanly with featureFlagsRefreshTrigger

Adding featureFlagsRefreshTrigger to ViewProjectContextType alongside the existing captcha/goals triggers is coherent and keeps the refresh plumbing centralised.


423-440: Default context value includes new trigger correctly

The defaultViewProjectContext initialises featureFlagsRefreshTrigger to 0, in line with other trigger defaults, so consumers can safely read it before any refresh occurs.


550-553: State hook for featureFlagsRefreshTrigger is correctly wired

Local state for featureFlagsRefreshTrigger parallels captchaRefreshTrigger and goalsRefreshTrigger, making it straightforward for FeatureFlagsView to subscribe via context.


3438-3438: Context provider now exposes featureFlagsRefreshTrigger

Passing featureFlagsRefreshTrigger through ViewProjectContext.Provider completes the wiring so any nested component (notably FeatureFlagsView) can react to refresh events.


4063-4064: Passing sdur into Pageflow is straightforward and non‑breaking

sdur={activeSession?.details?.sdur} cleanly forwards the (optional) session duration to Pageflow. As the prop is optional at the call site, it should remain backward‑compatible with existing Pageflow behaviour.


4339-4345: FeatureFlagsView render block is aligned with GoalsView pattern

Rendering FeatureFlagsView when activeTab === PROJECT_TABS.featureFlags and passing period, from, to, and timezone matches the existing GoalsView integration, so it will respect the project’s current time context.

Once FeatureFlagsView is in place, this should behave as expected.


1473-1520: Feature Flags tab definition integrates well into the tab array

The new tab entry uses PROJECT_TABS.featureFlags as ID with proper type safety, and pairs it with t('dashboard.featureFlags') and FlagIcon consistent with other tabs. The constant is properly defined in web/app/lib/constants/index.ts for both selfhosted and production configurations, ensuring the integration is sound.

backend/apps/cloud/src/project/entity/project.entity.ts (1)

12-12: LGTM!

The FeatureFlag relation follows the established pattern used by other entity relations in this file (e.g., goals, alerts, funnels). The @ApiProperty decorator with lazy loading syntax and the @OneToMany relation are correctly configured.

Also applies to: 132-134

backend/migrations/clickhouse/2025_12_08_feature_flags.js (1)

1-20: LGTM!

The migration follows ClickHouse best practices:

  • MergeTree engine is appropriate for append-only evaluation logs
  • Monthly partitioning with 1-year TTL is sensible for analytics data retention
  • ORDER BY (pid, flagId, created) supports efficient queries by project and flag
  • FixedString(12) for pid aligns with the project ID format used elsewhere

The denormalization of flagKey alongside flagId is acceptable for query efficiency.

backend/migrations/clickhouse/selfhosted_2025_12_10_feature_flags.js (1)

22-36: LGTM!

The feature_flag_evaluations table schema is consistent with the cloud migration, ensuring parity between cloud and self-hosted deployments.

backend/apps/community/src/app.module.ts (1)

14-14: LGTM!

The FeatureFlagModule is correctly imported and registered in the modules array, making feature flags available to all Community Edition instances as stated in the PR objectives.

Also applies to: 70-70

backend/apps/community/src/feature-flag/feature-flag.module.ts (1)

1-15: LGTM!

The module is well-structured and follows NestJS conventions. The dependencies are appropriate for the community edition's architecture:

  • ProjectModule for project validation
  • AnalyticsModule for ClickHouse access
  • AppLoggerModule for logging

The absence of TypeOrmModule is correct since the community edition stores feature flags directly in ClickHouse rather than MySQL.

backend/apps/cloud/src/feature-flag/feature-flag.module.ts (1)

1-24: LGTM!

The module is correctly configured for the cloud architecture:

  • TypeOrmModule.forFeature([FeatureFlag]) properly registers the entity for repository injection
  • UserModule inclusion enables user-based authorization for feature flag management
  • AnalyticsModule provides ClickHouse access for evaluation tracking

The additional dependencies compared to the community module (TypeOrmModule, UserModule) reflect the cloud edition's use of MySQL for persistent entities and more sophisticated access control.

backend/apps/cloud/src/app.module.ts (1)

24-24: LGTM! Clean module integration.

The FeatureFlagModule is properly imported and registered following NestJS conventions, consistent with other modules in the application.

Also applies to: 84-84

backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1)

1-34: LGTM! Well-structured type definitions.

The interfaces are cleanly defined with clear separation between standard TypeScript types and ClickHouse-optimized types. The enum values align with the MySQL migration, and the TargetingRule interface provides good documentation of supported columns.

backend/apps/cloud/src/analytics/analytics.controller.ts (1)

1119-1123: LGTM! Consistent refactor of session recording.

All call sites have been updated to use the renamed recordSessionActivity() method with the simplified signature (removing the isNew parameter). The changes are consistent across error logging, custom events, heartbeat, pageview, and noscript endpoints.

Also applies to: 1258-1262, 1346-1346, 1381-1385, 1526-1530

backend/apps/cloud/src/feature-flag/entity/feature-flag.entity.ts (1)

1-70: LGTM! Entity definition aligns with migration schema.

The TypeORM entity is well-structured with proper decorators, matches the MySQL migration schema, and includes Swagger documentation via ApiProperty. The @Unique(['project', 'key']) constraint correctly mirrors the database constraint.

Note: The project ManyToOne relation is nullable by default in TypeORM, which aligns with the nullable projectId in the migration. If feature flags should always belong to a project, consider adding { nullable: false } to the relation decorator and updating the migration accordingly.

backend/apps/community/src/analytics/analytics.service.ts (2)

1234-1281: LGTM! Simplified session tracking with cleaner API.

The refactor improves the codebase by:

  • Renaming recordSession to recordSessionActivity for clarity
  • Removing the isNew parameter to simplify the API
  • Streamlining the logic to focus on recording firstSeen/lastSeen timestamps
  • Maintaining proper error handling

The trade-off of querying for existing sessions on every call instead of using the isNew flag is acceptable given ClickHouse's performance characteristics and the simplicity gained.


3906-3907: LGTM! Profile tracking enhancements.

The addition of profileId and computed isIdentified flag enables user identification and profile-based analytics. The isIdentified logic correctly distinguishes user-supplied profiles (prefixed with usr_) from anonymous ones.

Also applies to: 3924-3925

backend/migrations/mysql/2025_12_08_feature_flags.sql (1)

11-11: Make projectId NOT NULL or clarify if project-agnostic flags are intentional.

The database schema allows nullable projectId, but the API enforces it as required (pid in CreateFeatureFlagDto has no @IsOptional()) and the controller explicitly assigns the project before saving. The entity relationship also declares project without optional typing. This inconsistency means:

  • The API guarantees every flag belongs to a project
  • The schema permits orphaned flags with projectId = NULL
  • With the unique constraint on (projectId, key), multiple flags can share the same key when projectId IS NULL (standard SQL NULL behavior)

If all flags must belong to a project, add NOT NULL to the migration. If project-agnostic flags are supported, update the entity, service, and API to explicitly allow it.

backend/apps/cloud/src/feature-flag/feature-flag.service.ts (4)

1-17: LGTM!

The service is properly structured with dependency injection via @InjectRepository. Imports are appropriate for the functionality.


19-38: LGTM!

Pagination implementation with sensible defaults (take: 100, skip: 0) and consistent ordering by key.


147-184: Permissive targeting behavior may be unexpected.

When attributes is undefined or when a specific attribute is missing, the method returns true (permissive). This means:

  1. If no attributes are provided, all targeting rules are bypassed
  2. If a specific attribute is missing, that rule is skipped

This could lead to users seeing flags they shouldn't if attributes fail to be derived. Consider whether this is the intended behavior or if a more restrictive default (return false when required attributes are missing) would be safer.


199-224: Rollout percentage calculation is correct.

The deterministic hash using MD5 with flag key and profile ID ensures consistent bucketing. The normalization logic using 0xffffffff (2^32-1) is correct for converting 8 hex chars to a 0-100 range.

Note: Using < instead of <= for the comparison means a 50% rollout will include profiles with normalized values 0-49.99..., which is mathematically correct.

backend/apps/cloud/src/feature-flag/feature-flag.controller.ts (2)

56-75: LGTM!

Controller setup with proper dependency injection of required services. Route versioning with both /feature-flag and /v1/feature-flag paths is appropriate.


490-501: ClickHouse query uses parameterized queries correctly.

Good use of {pid:FixedString(12)}, {flagId:String}, etc. for SQL injection prevention.

backend/apps/community/src/feature-flag/feature-flag.service.ts (1)

6-11: These lodash imports are actually used in the file at lines 251–265 (_reduce, _filter, and _keys appear in the update logic), so they are not unused. No changes needed.

Likely an incorrect or invalid review comment.

@Blaumaus Blaumaus merged commit 6aadc62 into swetrix-revamp Dec 10, 2025
2 checks passed
This was referenced Dec 14, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 21, 2025
9 tasks
@coderabbitai coderabbitai bot mentioned this pull request Feb 11, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant